/*
* Exposure (http://http://exposure.blogocracy.org/)
* Copyright (c) 2010 Kristoffer Jelbring
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
(function ($) {
    /**
    * @name Exposure
    * @author Kristoffer Jelbring (kris@blogocracy.org)
    * @version 0.9.1
    *
    * @type jQuery
    * @cat plugins/Media
    *
    * @desc Turn a simple HTML list into a rich and smart photo viewer that handles very large amounts of photos.
    *
    * @example $('#images').exposure({options});
    *
    * @options
    *	target:	(selector string) Where to insert the image being displayed. Defaults to '#exposure'. If no target is found, one will be created.
    *	showThumbs:	(boolean) Display thumbnails or not. Defaults to true. 
    *	showControls: (boolean) Display paging controls or not. Defaults to true, but will be set to false if missing controlsTarget or if carouselControl is set to true.
    *	imageControls: (boolean) Switch paging controls to use images instead of pages. Defaults to false.
    *	controls: (object) Display only certain paging controls. All controls default to true. Usage example: controls : { prevNext : true, pageNumbers : true, firstLast : false }
    *	carouselControls: (boolean) Enable carousel type controls instead of the classic paging type controls. Defaults to false, but will be set to false if showThumbs is also set to false.
    *	enableSlideshow: (boolean) Enable slideshow. Defaults to true.
    *	slideshowControlsTarget: (selector string) Where to insert the slideshow controls. Defaults to null.
    *	autostartSlideshow: (boolean) Automatically start the slideshow when the gallery is loaded. Defaults to false.
    *	slideshowDelay: (number) Delay for each slide in the slideshow (in milliseconds). Defauts to 3000.
    *	onSlideshowPlayed: (function) Callback funcation that is called when the slideshow is played.
    *	onSlideshowPaused: (function) Callback funcation that is called when the slideshow is paused.
    *	showCaptions: (boolean) Display captions or not. Captions are added by setting a title attribute on the items in the list.
    *	showExtraData: (boolean) Display extra image data or not. This data is added by inserting inner HTML to the items in the list.
    *	dataTarget: (selector string) Where to insert captions and extra image data. Defaults to null, in which case the data container will appended to the main Exposure target.
    *	controlsTarget: (selector string) Where to insert the paging controls. Defaults to null.
    *	onThumb: (function) Callback function that is called when a thumbnail is displayed.
    *	onImage: (function) Callback function that is called when an image is displayed. Defaults to removing the previous image.
    *	onCarousel: (function) Callback function that is called right before the image carousel is updated.
    *	onNext: (function) Callback function that is called when nextImage is called.
    *	onPrev: (function) Callback function that is called when prevImage is called.
    *	onPageChanged: (function) Callback function that is called when goToPage is called. Is not called when carouselControls is set. Defaults to showing all thumbnails on the current page.
    *	onPagingLink: (function) Callback function that is called when a new paging link has been added. Defaults to returning the link.
    *	separatePageBrowsing: (boolean) Enable separate page browsing (change page without changing the image being viewed). Defaults to false.
    *	loop: (boolean) Start over when last image is reached.
    *	onEndOfLoop: (function) Callback function that is called when the last image is reached and loop option is set to false.
    *	viewFirstImage: (boolean) Enable automatic showing of the first image in the gallery when the gallery is loaded. Defaults to true.
    *	pageSize: (number) Maximum number of images (thumbnails) per page. Defaults to 5.
    *	visiblePages: (number) Maxium number of pages visible in paging.
    *	preloadBuffer: (number) Maximum number of images to keep in load queue at any given time. Defaults to 3.
    *	keyboardNavigation: (boolean) Enable keyboard navigation. Defaults to true.
    *	clickingNavigation: (boolean) Enable browsing by clicking the image being shown. Defaults to true.
    *	fixedContainerSize: (boolean) Enable a fixed size target element (set the size using CSS) instead of one that adapts to the size of the current image. Defaults to false.
    *	maxWidth: (number) Maximum image width in the gallery (larger images will be downscaled). Defaults to null.
    *	maxHeight: (number) Maximum image height in the gallery (larger images will be downscaled). Defaults to null.
    *	stretchToMaxSize: (boolean) Stretch all images to maxWidth and maxHeight. Defaults to false.
    *	fullScreen: (boolean) Stretch all images to be viewn in full screen. Defaults to false.
    *	onEnterFullScreen: (function) Callback function that is called when entering full screen mode. Defaults to showing background mask.
    *	onExitFullScreen: (function) Callback function that is called when exiting full screen mode. Defaults to hiding target and background mask.
    *	showThumbToolTip: (boolean) Display captions as thumbnail tooltips or not. Defaults to true.
    *	onEmpty: (function) Called when the gallery is empty. Defaults to removing controls and targets and to hiding the list element that the plugin is called on.
    *	onInit: (function) Called when the gallery has been initialized.
    *	allowDuplicates: (boolean) Allow the same image to be added more than once. Defaults to true.
    *	jsonSource: (JSON data string/URL to JSON data/JSON object) Load additional images from an external source using JSON. Defaults to null.
    */
    var $$ = $.fn.exposure = function ($args) {

        var v = "0.9.1";
        var i;

        var $defaults = {
            target: '#exposure',
            showThumbs: true,
            showControls: true,
            imageControls: false,
            controls: {
                prevNext: true,
                firstLast: true,
                pageNumbers: true
            },
            carouselControls: false,
            enableSlideshow: true,
            slideshowControlsTarget: null,
            autostartSlideshow: false,
            slideshowDelay: 3000,
            onSlideshowPlayed: function () { },
            onSlideshowPaused: function () { },
            showCaptions: true,
            showExtraData: true,
            dataTarget: null,
            controlsTarget: null,
            onThumb: function (thumb) { },
            onImage: function (image, imageData, thumb) {
                $('.exposureWrapper > .exposureLastImage').remove();
            },
            onCarousel: function (firstImage, lastImage) { },
            onNext: function () { },
            onPrev: function () { },
            onPageChanged: function () {
                $('.exposureThumbs li.current').show().each(function (i) {
                    var imageHeight = $(this).find('img').height();
                    if (imageHeight > 0) {
                        $(this).height(imageHeight);
                    }
                });
            },
            onPagingLink: function (link) {
                return link;
            },
            separatePageBrowsing: false,
            loop: true,
            onEndOfLoop: function () { },
            pageSize: 5,
            viewFirstImage: true,
            visiblePages: 5,
            preloadBuffer: 3,
            keyboardNavigation: true,
            clickingNavigation: true,
            fixedContainerSize: false,
            maxWidth: null,
            maxHeight: null,
            stretchToMaxSize: false,
            fullScreen: false,
            onEnterFullScreen: function (mask) {
                mask.show();
            },
            onExitFullScreen: function (target, mask) {
                target.hide();
                mask.hide();
            },
            showThumbToolTip: true,
            onEmpty: function () {
                $('.exposureThumbs').hide();
                $($.exposure.target).remove();
                if ($.exposure.showControls) {
                    $($.exposure.controlsTarget).remove();
                }
                if ($.exposure.slideshowControlsTarget) {
                    $($.exposure.slideshowControlsTarget).remove();
                }
            },
            onInit: function () { },
            allowDuplicates: true,
            jsonSource: null
        };

        var opts = $.extend($defaults, $args);
        for (i in opts) {
            if ($$.defined($defaults[i])) {
                $.exposure[i] = opts[i];
            }
        }

        if (!$($.exposure.target).length) {
            // The target element is missing so it needs to be created.
            $('<div id="exposure"></div>').insertBefore($(this));
        }

        var wrapper = $('<div class="exposureWrapper"></div>');
        var target = $($.exposure.target).addClass('exposureTarget').append(wrapper);

        if ($.exposure.showCaption || $.exposure.showExtraData) {
            // Determine which image data to display (caption and/or additional data).
            var dataElementsHtml = '';
            if ($.exposure.showCaptions) {
                dataElementsHtml += '<div class="caption"></div>';
            }
            if ($.exposure.showExtraData) {
                dataElementsHtml += '<div class="extra"></div>';
            }

            // Append image data container.
            var dataElements = $(dataElementsHtml);
            if (dataElements.length) {
                if ($.exposure.dataTarget && $($.exposure.dataTarget).length) {
                    $($.exposure.dataTarget).addClass('exposureData').append(dataElements);
                } else {
                    $.exposure.dataTarget = null;
                    target.append($('<div class="exposureData"></div>').append(dataElements));
                }
            }
        }

        // Don't use carousel controls if not showing thumbs.
        if (!$.exposure.showThumbs) {
            $.exposure.carouselControls = false;
        }

        // Don't show paging controls if using carousel controls, or if there is no controls target or if all individual controls have been turned off.
        if ($.exposure.carouselControls || !$.exposure.controlsTarget || (!$.exposure.controls.prevNext && !$.exposure.controls.firstLast && !$.exposure.controls.pageNumbers)) {
            $.exposure.showControls = false;
        }

        // Render controls.
        if ($.exposure.showControls) {
            $($.exposure.controlsTarget).addClass('exposureControls').each(function () {
                if ($.exposure.controls.firstLast) { $(this).append($('<a class="exposureFirstPage" href="javascript:void(0);">' + $.exposure.texts.first + '</a>').click($.exposure.first)); }
                if ($.exposure.controls.prevNext) { $(this).append($('<a class="exposurePrevPage" href="javascript:void(0);">' + $.exposure.texts.previous + '</a>').click($.exposure.prev)); }
                if ($.exposure.controls.pageNumbers) { $(this).append($('<div class="exposurePaging"></div>')); }
                if ($.exposure.controls.prevNext) { $(this).append($('<a class="exposureNextPage" href="javascript:void(0);">' + $.exposure.texts.next + '</a>').click($.exposure.next)); }
                if ($.exposure.controls.firstLast) { $(this).append($('<a class="exposureLastPage" href="javascript:void(0);">' + $.exposure.texts.last + '</a>').click($.exposure.last)); }
            });
        }

        // Only render slideshow controls if there is a slideshow controls target.
        if ($.exposure.enableSlideshow && $.exposure.slideshowControlsTarget) {
            $($.exposure.slideshowControlsTarget).addClass('exposureSlideshowControls').each(function () {
                $(this).append($('<a class="exposurePlaySlideshow" href="javascript:void(0);">' + $.exposure.texts.play + '</a>').click($.exposure.playSlideshow));
                $(this).append($('<a class="exposurePauseSlideshow" href="javascript:void(0);">' + $.exposure.texts.pause + '</a>').hide().click($.exposure.pauseSlideshow));
            });
        }

        // Bind keys for navigation (using Hotkeys Plugin).
        if ($.exposure.keyboardNavigation) {
            $(document).bind('keyup', 'left', $.exposure.prevImage);
            $(document).bind('keyup', 'right', $.exposure.nextImage);
            $(document).bind('keyup', 'ctrl+left', $.exposure.prevPage);
            $(document).bind('keyup', 'ctrl+right', $.exposure.nextPage);
            $(document).bind('keyup', 'up', $.exposure.lastImage);
            $(document).bind('keyup', 'down', $.exposure.firstImage);
            $(document).bind('keyup', 'ctrl+up', $.exposure.lastPage);
            $(document).bind('keyup', 'ctrl+down', $.exposure.firstPage);
            if ($.exposure.enableSlideshow) {
                $(document).bind('keyup', 'space', $.exposure.toggleSlideshow);
            }
        }

        if ($.exposure.fullScreen) {
            $(window).resize($.exposure.fitToWindow);
            $('<div class="exposureMask"></div>').click($.exposure.exitFullScreen).insertAfter(target);
            if ($.exposure.keyboardNavigation) {
                $(document).bind('keyup', 'esc', $.exposure.exitFullScreen);
            }
        }

        var jsonImages = null;

        if ($.exposure.jsonSource) {
            if ($$.object($.exposure.jsonSource)) {
                jsonImages = $.exposure.jsonSource;
            } else if ($.exposure.jsonSource.length) {
                if ($$.startsWith($.exposure.jsonSource, "http://") || $$.startsWith($.exposure.jsonSource, "https://")) {
                    // Fetch JSON images using AJAX from specified URL source.
                    jsonImages = $.ajax({ url: $.exposure.jsonSource,
                        type: 'GET',
                        async: false
                    }).responseText;
                } else {
                    jsonImages = $.exposure.jsonSource;
                }
            }
        }

        // Return "this" to maintain chainability.
        return this.addClass('exposureThumbs').each(function () {
            var i;
            if (jsonImages) {
                var images = $$.object(jsonImages) ? jsonImages : $.parseJSON(jsonImages);
                if (images && images.data) {
                    // Append images fetched from JSON source to the list of images.
                    for (i in images.data) {
                        var photo = images.data[i];
                        if (photo.source && photo.source.length) {
                            var item = $('<li></li>');
                            var link = $('<a></a>').attr('href', photo.source);
                            if (photo.thumb_source && photo.thumb_source.length) {
                                var thumb = $('<img />').attr('src', photo.thumb_source);
                                if (photo.caption && photo.caption.length) {
                                    thumb.attr('title', photo.caption);
                                }
                                link.append(thumb);
                            } else if (photo.caption && photo.caption.length) {
                                link.attr('title', photo.caption);
                            }
                            item.append(link);
                            if (photo.extra_data && photo.extra_data.length) {
                                item.append($(photo.extra_data));
                            }
                            $(this).append(item);
                        }
                    }
                }
            }

            var foundImage = false;
            var foundThumb = false;

            if ($(this).children('li').length) {
                var selectedIndex = null;

                $(this).show().children('li').each(function () {
                    foundImage = true;

                    // The a tag contains all the needed information about the image.
                    var a = $(this).find('a');
                    if (a.length) {
                        // Use only the first matching link.
                        a = $(a[0]);

                        var src = a.attr('href');
                        var img = a.find('img');

                        // Get caption and thumbnail source from either nested img tag or from rel attribute.
                        var thumbSrc = img.length ? img.attr('src') : a.attr('rel');
                        var caption = img.length ? img.attr('title') : a.attr('title');

                        var isSelected = a.hasClass('selected') && !selectedIndex;

                        // Remove link and extract additional image data.
                        a.remove();
                        var thumbData = $(this).html();

                        if (thumbSrc) {
                            foundThumb = true;
                        }

                        // All information extracted, remove original list entry.
                        $(this).remove();

                        // Add image to list of images.
                        var imageIndex = $$.newImage(src, thumbSrc, caption, thumbData);

                        if (imageIndex > -1) {
                            if (isSelected) {
                                selectedIndex = imageIndex;
                            }

                            if ($$.loadQueue.length < $.exposure.preloadBuffer) {
                                // Preload buffer hasn't been filled yet, add image to load queue.				
                                $$.addToLoadQueue(imageIndex);
                            }
                        }
                    } else {
                        // Just remove this empty entry.
                        $(this).remove();
                    }
                });

                if (!$.exposure.showThumbs) {
                    // Thumbnails are turned off, change page size to 1.
                    $.exposure.pageSize = 1;

                    // Remove the thumbnails container.
                    $('.exposureThumbs').remove();
                }

                if (foundImage) {
                    // Start preloading the first image.
                    $$.preloadNextInQueue();

                    $$.createPaging();

                    if (selectedIndex) {
                        $.exposure.goToPage($.exposure.pageNumberForImage(selectedIndex));
                        $.exposure.viewImage(selectedIndex);
                    } else {
                        // View the first page (and the first image).
                        $.exposure.goToPage(1);
                    }

                    if ($.exposure.enableSlideshow && $.exposure.autostartSlideshow) {
                        $.exposure.playSlideshow();
                    }
                } else {
                    $.exposure.onEmpty();
                }
            } else {
                $.exposure.onEmpty();
            }

            $.exposure.onInit();

            $$.initialized = true;
        });
    };

    // Private functions and properties. These are only for internal use.

    /**
    * Check if a variable is defined.
    *
    * @param v Variable to check.
    */
    $$.defined = function (v) {
        return typeof v !== 'undefined';
    };

    /**
    * Check if a variable is an object.
    *
    * @param v Variable to check.
    */
    $$.object = function (v) {
        return typeof v === 'object';
    };

    /**
    * Check if a string starts with another string.
    *
    * @param s1 String to check.
    * @param s2 String to look for.
    */
    $$.startsWith = function (s1, s2) {
        if (s1 && s2) {
            return s1.match("^" + s2) === s2;
        }
        return false;
    };

    /**
    * Calculate the differance in outerwidth and width of an element.
    *
    * @param el The element to check.
    * @returns Width differance.
    */
    $$.widthDiff = function (el) {
        return el ? el.outerWidth(true) - el.width() : 0;
    };

    /**
    * Calculate the differance in outerHeight and height of an element.
    *
    * @param el The element to check.
    * @returns Height differance.
    */
    $$.heightDiff = function (el) {
        return el ? el.outerHeight(true) - el.height() : 0;
    };

    /**
    * Value object representing an image in the viewer.
    *
    * @param src Source to the full size image.
    * @param thumb Source to thumbnail version of the image.
    * @param caption Image caption.
    * @param data Extra image data.
    */
    $$.Image = function (src, thumb, caption, data) {
        this.src = src;
        this.thumb = thumb;
        this.caption = caption;
        this.data = data;
        this.loaded = false;
    };

    /**
    * All the images in the viewer. Holds an array of Image objects that are filled up when the plugin is loaded.
    */
    $$.images = [];

    /**
    * All the image sources that's been previously added to the viewer.
    */
    $$.sources = {};

    /**
    * Create a new Image object and add it to images array.
    *
    * @param src Source to the full size image.
    * @param thumb Source to thumbnail version of the image.
    * @param caption Image caption.
    * @param data Extra image data.
    * @returns Index of the new image.
    */
    $$.newImage = function (src, thumb, caption, data) {
        var alreadyAdded = $$.defined($$.sources[src]);
        if (alreadyAdded && !$.exposure.allowDuplicates) {
            return -1;
        }
        var image = new $$.Image(src, thumb, caption, data);
        var imageIndex = $$.images.push(image) - 1;
        if (!alreadyAdded) {
            $$.sources[src] = imageIndex;
        }
        return imageIndex;
    };

    /**
    * Initialization flag.
    */
    $$.initialized = false;

    /**
    * Index of the image currently being viewed.
    */
    $$.current = -1;

    /**
    * Deselect the image currently being viewed.
    */
    $$.deselectCurrentImage = function () {
        $$.current = -1;
        $('.exposureThumbs li.active').removeClass('active');
    };

    /**
    * The load queue, holds an array of indices of images to load.
    */
    $$.loadQueue = [];

    /**
    * Add an image to the load queue.
    *
    * @param index Index of image to add.
    */
    $$.addToLoadQueue = function (index) {
        if (!$$.loaded(index) && !$$.queued(index)) {
            $$.loadQueue.push(index);
        }
    };

    /**
    * Check if a specific image exists in the load queue.
    *
    * @param index Index of image to check.
    */
    $$.queued = function (index) {
        return $.inArray(index, $$.loadQueue) > -1;
    };

    /**
    * Check if a specific image has been loaded.
    *
    * @param index Index of image to check.
    */
    $$.loaded = function (index) {
        var image = $.exposure.getImage(index);
        if (image !== null) {
            return image.loaded;
        }
        return false;
    };

    /**
    * Find the next, not already loaded image, in the load queue. This function is recursive and will continue until
    * an image is found, or until the queue is empty.
    */
    $$.nextInLoadQueue = function () {
        var i;
        if ($$.loadQueue.length > 0) {
            var next = $$.loadQueue.shift();
            if ($$.loaded(next)) {
                // Image already loaded, remove from load queue.
                i = $.inArray(index, $$.loadQueue);
                $$.loadQueue.splice(i, 1);

                // Find next in queue.
                return $$.nextInLoadQueue();
            }
            return next;
        }
        return null;
    };

    /**
    * Preload the next image in the load queue.
    */
    $$.preloadNextInQueue = function () {
        if ($$.loadQueue.length > 0) {
            var nextIndex = $$.nextInLoadQueue();
            if (nextIndex !== null) {
                $$.loadImage(nextIndex, $$.preloadNextInQueue);
            }
        }
    };

    /**
    * Load a specific page.
    *
    * @param page Number of the page to load.
    * @param imageToView Index of the image to view (defaults to viewing first image on page if this parameter isn't set).
    */
    $$.loadPage = function (page, imageToView) {
        if ($$.validPage(page)) {

            // Calculate first and last images on this page.
            var last = page * $.exposure.pageSize;
            var first = last - $.exposure.pageSize;

            if (last > $$.images.length) {
                last = $$.images.length;
            }

            $$.pageTransition = true;

            $$.viewThumbs(first, last - 1);

            if (!$.exposure.separatePageBrowsing) {
                if (imageToView) {
                    // Moving backwards, set the last image on the page as active.
                    $.exposure.viewImage(imageToView);
                } else {
                    if (page > 1 || ((page === 1 && $.exposure.viewFirstImage) || $$.initialized)) {
                        // Set the first image on this page as active.			
                        $.exposure.viewImage(first);
                    }
                }
            }

            $$.pageTransition = false;
        }
    };

    /**
    * Views thumbnails for a specific set of images (and creates them if needed).
    *
    * @param first Index of the first image to view.
    * @param last Index of the last image to view.
    */
    $$.viewThumbs = function (first, last) {
        var i;
        if ($.exposure.showThumbs) {

            // Go through images in set.
            for (i = first; i <= last; i++) {
                $$.viewThumb(i, i === first, i === last, true);
            }
            if (!$.exposure.carouselControls && $$.currentPage < $.exposure.numberOfPages()) {
                // Preload next page of thumbnails.
                var firstNext = last + 1;
                var lastNext = last + $.exposure.pageSize;
                if (lastNext >= $$.images.length) {
                    lastNext = $$.images.length - 1;
                }

                for (i = firstNext; i <= lastNext; i++) {
                    var container = $$.viewThumb(i, i === firstNext, i === lastNext, false);
                    if (container && container.length) {
                        container.hide();
                    }
                }
            }
        }
    };

    /**
    * View thumbnail for a specific image (and create it if needed).
    *
    * @param index Index of the image to view.
    * @param first If the image is the first on the page.
    * @param last If the image is the last on the page.
    * @param current If the image is a part of the current page.
    */
    $$.viewThumb = function (index, first, last, currentPage) {
        // Make sure image index is in scope.
        if (index < 0) {
            index = $$.images.length + index;
        } else if (index >= $$.images.length) {
            index = index - $$.images.length;
        }

        var image = $$.images[index];
        // Find thumbnail container.
        var container = $.exposure.getThumb(index).parent();
        if (!container.length) {
            // Create a thumbnail if one doesn't already exist.
            container = $$.createThumbForImage(image, index);

            // Add page number as rel attribute.
            container.attr('rel', $.exposure.pageNumberForImage(index));
        }
        if (container.length) {
            // Append in the end of the container in order to save the ordering of the images.
            container.parent().append(container);

            if (first) {
                // Decorate thumbnail container for first image on page.
                container.addClass('first');
            } else {
                container.removeClass('first');
            }
            if (last) {
                // Decorate thumbnail container for last image on page.
                container.addClass('last');
            } else {
                container.removeClass('last');
            }

            if (currentPage) {
                if ($.exposure.carouselControls) {
                    container.show();
                } else {
                    container.addClass('current');
                }
            }
        }

        return container;
    };

    /**
    * Load a specific image.
    *
    * @param index Index of image to load.
    * @param onload Image onload callback function.
    */
    $$.loadImage = function (index, onload) {
        var image = $.exposure.getImage(index);
        var img = $('<img />').addClass('exposureImage');
        var i;
        if (image !== null) {
            image.loaded = true;
            if ($$.queued(index)) {
                // Since image already has been loaded, remove it from the load queue.
                i = $.inArray(index, $$.loadQueue);
                $$.loadQueue.splice(i, 1);
            }
            if (typeof onload === 'function') {
                img.load(onload);
            }
            img.attr('src', image.src);
        }
        return img;
    };

    /**
    * Create a thumbnail for a specific image.
    *
    * @param image Image object for the image.
    * @param image Index of the image.
    */
    $$.createThumbForImage = function (image, index) {
        if ($.exposure.showThumbs) {
            var thumb = $.exposure.getThumb(index);

            if (thumb === null || !thumb.length) {
                // Create thumbnail container.
                var container = $('<li></li>');
                $('.exposureThumbs').append(container);

                // Create thumbnail img element.
                thumb = $('<img />');

                if (image.thumb) {
                    thumb.attr('src', image.thumb);
                } else {
                    // Create a thumbnail from the original image.
                    thumb.attr('src', image.src);

                    // Downscale the new thumbnail.
                    var imageWidth = Math.ceil(thumb.width() / thumb.height() * container.height());
                    var imageHeight = Math.ceil(thumb.height() / thumb.width() * container.width());
                    if (imageWidth < imageHeight) {
                        thumb.css({ height: 'auto', maxWidth: container.width() });
                    } else {
                        thumb.css({ width: 'auto', maxHeight: container.height() });
                    }
                }

                container.append(thumb.css('display', 'block'));

                // Add image index and caption as attributes.
                thumb.attr('rel', index);
                if (image.caption && $.exposure.showThumbToolTip) {
                    thumb.attr('title', image.caption);
                }

                // Save extra image data in thumbnail data.
                thumb.data('data', image.data);

                thumb.click(function () {
                    // When a thumbnail is clicked, view full version of that image.
                    $.exposure.viewImage(Number($(this).attr('rel')));
                });

                thumb.load(function () {
                    // Set the height of the thumbnail container to the height of the thumbnail.
                    var imageHeight = $(this).height();
                    if (imageHeight > 0) {
                        $(this).parent().height(imageHeight);
                    }
                });

                $.exposure.onThumb(thumb);

                return container;
            }
        }
        return null;
    };

    /**
    * Number of the page currently being viewed.
    */
    $$.currentPage = 1;

    /**
    * Check if a specific page number is a valid page number.
    *
    * @param page Page number to check.
    */
    $$.validPage = function (page) {
        return page > 0 && page <= $.exposure.numberOfPages();
    };

    /**
    * Create paging links.
    */
    $$.createPaging = function () {
        var i;
        if ($.exposure.showControls && $.exposure.controls.pageNumbers) {
            // Create paging links.
            var stop = $.exposure.imageControls ? $.exposure.numberOfImages() : $.exposure.numberOfPages();
            $('.exposurePaging').each(function () {
                for (i = 1; i <= stop; i++) {
                    $(this).append($$.newPagingLink(i));
                }
            });
        }
    };

    /**
    * Update paging links.
    */
    $$.updatePaging = function (newActivePage) {
        if ($.exposure.showControls && $.exposure.controls.pageNumbers) {
            var current = $.exposure.imageControls ? $$.current + 1 : $$.currentPage;
            $('.exposurePaging span.active').each(function () {
                $(this).replaceWith($$.newPagingLink(current));
            });
            $('.exposurePaging a[rel="' + newActivePage + '"]').each(function () {
                $(this).replaceWith($('<span>' + newActivePage + '</span>').addClass('active'));
            });
            var pageCount = $.exposure.imageControls ? $.exposure.numberOfImages() : $.exposure.numberOfPages();
            if ($.exposure.visiblePages > 0 && pageCount > $.exposure.visiblePages) {
                var firstVisiblePage = newActivePage;
                var lastVisiblePage = $.exposure.visiblePages;
                var flooredVisiblePages = Math.floor($.exposure.visiblePages / 2);
                if (newActivePage <= flooredVisiblePages) {
                    firstVisiblePage = 1;
                } else if (newActivePage > (pageCount - flooredVisiblePages)) {
                    lastVisiblePage = pageCount;
                    firstVisiblePage = lastVisiblePage - $.exposure.visiblePages + 1;
                } else {
                    firstVisiblePage -= flooredVisiblePages;
                    lastVisiblePage = firstVisiblePage + $.exposure.visiblePages - 1;
                }
                $('.exposurePaging').each(function () {
                    $(this).children().each(function (i) {
                        var currentPage = i + 1;
                        if (currentPage >= firstVisiblePage && currentPage <= lastVisiblePage) {
                            $(this).show();
                        } else {
                            $(this).hide();
                        }
                    });
                });
            }
        }
    };

    /**
    * Create a new paging link for a specific page.
    *
    * @param page Index of the image/number of the page (depending on the imageControls setting) to create the link for.
    */
    $$.newPagingLink = function (index) {
        return $.exposure.onPagingLink($('<a href="javascript:void(0);" rel="' + index + '">' + index + '</a>').click(function () {
            // View the image/page defined in the rel attribute of the link.
            var rel = Number($(this).attr('rel'));
            if ($.exposure.imageControls) {
                $.exposure.viewImage(rel - 1);
            } else {
                $.exposure.goToPage(rel);
            }
        }));
    };

    /**
    * Page transition state.
    */
    $$.pageTransition = false;

    /**
    * Slideshow playing state.
    */
    $$.playingSlideshow = false;

    /**
    * Holds the timer for the slideshow.
    */
    $$.slideshowTimer = null;

    /**
    * Slideshow transition state.
    */
    $$.slideshowTransition = false;

    /**
    * Recursive function that runs nextImage() after given delay. Don't use this directly, use playSlideshow() instead.
    */
    $$.slideshow = function () {
        $$.slideshowTimer = setTimeout(function () {
            $$.slideshowTransition = true;
            $.exposure.nextImage();
            $$.slideshowTransition = false;
            $$.slideshow();
        }, $.exposure.slideshowDelay);
    };

    /**
    * Full screen state.
    */
    $$.infullScreen = false;

    /**
    * Calculate actual max width (subtract padding, margin and borders of image and container),
    */
    $$.actualMaxWidth = function (image, target) {
        return $.exposure.maxWidth ? $.exposure.maxWidth - ($$.widthDiff(image) + $$.widthDiff(target)) : 0;
    };

    /**
    * Calculate actual max height (subtract padding, margin and borders of image and container),
    */
    $$.actualMaxHeight = function (image, target) {
        return $.exposure.maxHeight ? $.exposure.maxHeight - ($$.heightDiff(image) + $$.heightDiff(target)) : 0;
    };

    /**
    * Fix image to max size.
    */
    $$.fitToMaxSize = function (image) {
        var target = $('.exposureTarget');
        if ($.exposure.stretchToMaxSize) {
            if ($.exposure.maxWidth) {
                // Stretch to maxWidth.
                image.width($$.actualMaxWidth(image, target));
            }
            if ($.exposure.maxHeight) {
                // Stretch to maxHeight.
                image.height($$.actualMaxHeight(image, target));
            }
        } else {
            if (image.width() > image.height()) {
                // Landscape format image, fit to width first.
                $$.fitToMaxWidth(image, target);
                $$.fitToMaxHeight(image, target);
            } else if (image.height() > image.width()) {
                // Portrait format image, fit to height first.
                $$.fitToMaxHeight(image, target);
                $$.fitToMaxWidth(image, target);
            } else {
                // Square format image.
                var actualMaxHeight = $$.actualMaxHeight(image, target);
                var smallest = $$.actualMaxWidth(image, target);
                if (!smallest || (actualMaxHeight && smallest && actualMaxHeight < smallest)) {
                    smallest = actualMaxHeight;
                }
                if (smallest && image.width() > smallest) {
                    image.width(smallest);
                    image.height(smallest);
                }
            }
        }
    };

    /**
    * Center main image in window.
    */
    $$.centerImageInWindow = function (image) {
        var target = $('.exposureTarget');
        //target.width(image.width()).height(image.height() + 30);
        target.width(image.width()).height(330);
        target.css({
            'top': ($(window).height() - target.outerHeight(true)) / 2,
            'left': ($(window).width() - target.outerWidth(true)) / 2
        });

        $('.exposureLastImage').each(function () {
            $(this).css({
                'top': (target.height() - $(this).height()) / 2,
                'left': (target.width() - $(this).width()) / 2
            });
        });
    };

    /**
    * Fit image to maxWidth.
    */
    $$.fitToMaxWidth = function (image, target) {
        var actualMaxWidth = $$.actualMaxWidth(image, target);
        if (actualMaxWidth && image.width() > actualMaxWidth) {
            // Calculate new height to maintain aspect ratio.								
            var newHeight = Math.round(actualMaxWidth * image.height() / image.width());
            image.height(newHeight);
            // Shrink to maxWidth.	
            image.width(actualMaxWidth);
        }
    };

    /**
    * Fit image to maxHeight.
    */
    $$.fitToMaxHeight = function (image, target) {
        var actualMaxHeight = $$.actualMaxHeight(image, target);
        if (actualMaxHeight && image.height() > actualMaxHeight) {
            // Calculate new width to maintain aspect ratio.								
            var newWidth = Math.round(actualMaxHeight * image.width() / image.height());
            image.width(newWidth);
            // Shrink to maxHeight.
            image.height(actualMaxHeight);
        }
    };

    /**
    * Resize target container element to fit image.
    */
    $$.resizeContainer = function (img) {
        // Resize image according to maxWidth and maxHeight settings.
        $$.fitToMaxSize(img);

        // Resize target element to fit image.
        if (!$.exposure.fixedContainerSize) {
           // $('.exposureTarget').show().width(img.width()).height(img.height + 50);
            $('.exposureTarget').show().width(img.width()).height(330);
        }
    };

    // Extend with public functions. These can be called from your gallery using $.exposure.nameOfFunction().
    $.extend({ exposure: {
        /**
        * Calculate the page number of a specific image.
        *
        * @param index Index of image to get page number for.
        */
        pageNumberForImage: function (index) {
            return Math.ceil((index + 1) / $.exposure.pageSize);
        },

        /**
        * Calculate the total number of pages using the set page size.
        */
        numberOfPages: function () {
            // Calculate the page number for the last image.
            return $.exposure.pageNumberForImage($$.images.length - 1);
        },

        /**
        * Check if the the page currently being viewed is the first page.
        */
        atFirstPage: function () {
            return $$.currentPage === 1;
        },

        /**
        * Check if the the page currently being viewed is the last page.
        */
        atLastPage: function () {
            return $$.currentPage === $.exposure.numberOfPages();
        },

        /**
        * Check if an image is the first image on its page.
        *
        * @param index Index of image to check. Will default to image currently being viewed if not set. 
        */
        firstImageOnPage: function (index) {
            if (!index) {
                index = $$.current;
            }
            return $.exposure.pageSize === 1 || (index % $.exposure.pageSize === 0);
        },

        /**
        * Check if the an image is the last image on its page.
        *
        * @param index Index of image to check. Will default to image currently being viewed if not set. 
        */
        lastImageOnPage: function (index) {
            if (!index) {
                index = $$.current;
            }
            var imageCount = $$.images.length;
            if ($.exposure.pageSize === 1 || imageCount === 1) {
                return true;
            }
            if (index > 0) {
                var currentPageSize = $.exposure.pageSize;
                var currentPage = $.exposure.pageNumberForImage(index);
                if (currentPage === $.exposure.numberOfPages()) {
                    // Calculate the size of the last page as it may differ from the set page size.
                    var newPageSize = imageCount % $.exposure.pageSize;
                    if (newPageSize > 0) {
                        currentPageSize = newPageSize;
                    }
                }

                var imageIndex = index;
                if (currentPage > 1) {
                    imageIndex -= (currentPage - 1) * $.exposure.pageSize;
                }

                // Check if the current image is the last image of the current page.				
                return (imageIndex + 1) % currentPageSize === 0;
            }
            return false;
        },

        /**
        * Get the number of the current page.
        */
        currentPage: function () {
            return $$.currentPage;
        },

        /**
        * Get the number of images.
        */
        numberOfImages: function () {
            return $$.images.length;
        },

        /**
        * Check if the image currently being viewed is the first image.
        */
        atFirstImage: function () {
            return $$.current === 0;
        },

        /**
        * Check if the image currently being viewed is the last image.
        */
        atLastImage: function () {
            return $$.current === $.exposure.numberOfImages() - 1;
        },

        /**
        * Get a spefic image object from the images array.
        *
        * @param index Index of image to get.
        */
        getImage: function (index) {
            if (index !== null && index > -1 && index < $$.images.length) {
                return $$.images[index];
            }
            return null;
        },

        /**
        * Get the index of the image with the specified image source.
        *
        * @param src Source of the image to get index for.
        */
        indexOfImage: function (src) {
            if (src && $$.defined($$.sources[src])) {
                return $$.sources[src];
            }
            return -1;
        },

        /**
        * Get the index of the current image.
        */
        currentImage: function () {
            return $$.current;
        },

        /**
        * Dynamically add an image to the gallery. 
        *
        * @param src Source to the full size image.
        * @param thumb Source to thumbnail version of the image.
        * @param caption Image caption.
        * @param data Extra image data.
        */
        addImage: function (src, thumb, caption, data) {
            var pageCount = $.exposure.numberOfPages();
            var index = $$.newImage(src, thumb, caption, data);
            if (index > -1) {
                var pageNumber = $.exposure.pageNumberForImage(index);
                var containers = $('.exposureThumbs li[rel="' + pageNumber + '"]');
                if (containers.length) {
                    containers.removeClass('last');
                }

                // Recreate paging if a new page needs to be added.
                var newPageAdded = pageNumber > pageCount;
                if (newPageAdded) {
                    // Make sure paging container is empty.
                    $('.exposurePaging').empty();

                    $$.createPaging();
                }

                if (newPageAdded || pageNumber === $$.currentPage) {
                    // Reload the current page.
                    $.exposure.goToPage($$.currentPage);
                }
            }
        },

        /**
        * Dynamically remove a specific image from the gallery.
        *
        * @param index Index of image to remove.
        */
        removeImage: function (index) {
            if ($$.images.length === 1) {
                $.exposure.removeAllImages();
            } else {
                if ($.exposure.enableSlideshow) {
                    $.exposure.pauseSlideshow();
                }

                var oldPageCount = $.exposure.numberOfPages();

                // Remove the image from the list of images.
                $$.images.splice(index, 1);

                // Remove the image from the loadQueue.
                var queueIndex = $.inArray(index, $$.loadQueue);
                if (queueIndex > -1) {
                    $$.loadQueue.splice(queueIndex, 1);
                }

                // Remove thumbnail and container.
                var container = $.exposure.getThumb(index).parent();
                container.remove();

                // Update thumbnail containers.
                $('.exposureThumbs > li').each(function (i) {
                    if (i >= index) {
                        // Update page number in rel attribute.
                        var newRel = $.exposure.pageNumberForImage(i);
                        $(this).attr('rel', newRel);

                        // Update index number in rel attribute of image.
                        $(this).find('img').attr('rel', i);

                        // Update first/last classes
                        if ($.exposure.firstImageOnPage(i)) {
                            $(this).addClass('first');
                        } else {
                            $(this).removeClass('first');
                        }
                        if ($.exposure.lastImageOnPage(i)) {
                            $(this).addClass('last');
                        } else {
                            $(this).removeClass('last');
                        }

                        if ($$.currentPage === newRel) {
                            $(this).show();
                        } else {
                            $(this).hide();
                        }
                    }
                });

                // Recreate paging links.
                var pageRemoved = $.exposure.numberOfPages < oldPageCount;
                if (pageRemoved) {
                    // Make sure paging container is empty.
                    $('.exposurePaging').empty();

                    $$.createPaging();
                }

                if ($$.current === index) {
                    // Skip to next image if the deleting image was the currently viewed image.
                    $$.current = -1;
                    var nextIndex = index;
                    if (index === $.exposure.numberOfImages()) {
                        nextIndex = 0;
                    }
                    $.exposure.viewImage(nextIndex);
                }
            }
        },

        /**
        * Removes all images from the gallery. Usable when dynamically rebuilding the gallery from scratch.
        */
        removeAllImages: function () {
            $$.images = [];
            $$.sources = {};
            $$.loadQueue = [];
            if ($.exposure.enableSlideshow) {
                $.exposure.pauseSlideshow();
            }
            $('.exposureThumbs').empty();
            $('.exposurePaging').empty();
            $.exposure.viewImage(0);
        },

        /**
        * Get the thumbnail img element for a specific image.
        *
        * @param index Index of image to find thumbnail for.
        */
        getThumb: function (index) {
            return $('.exposureThumbs img[rel="' + index + '"]');
        },

        /**
        * Get the index of the next image.
        */
        getNextImage: function () {
            if ($$.current === $$.images.length - 1) {
                // Is at last image, return first image.
                if ($.exposure.loop) {
                    return 0;
                } else {
                    // Loop ended callback.
                    $.exposure.onEndOfLoop();
                }
            } else {
                // Return next image.
                return $$.current + 1;
            }
            return null;
        },

        /**
        * Get the index of the previous image.
        */
        getPrevImage: function () {
            if ($$.current === 0) {
                // Is at first image, return last image.
                if ($.exposure.loop) {
                    return $$.images.length - 1;
                }
            } else {
                // Return previous image. 
                return $$.current - 1;
            }
            return null;
        },

        /**
        * View a specific page.
        *
        * @param page Number of the page to view.
        * @param imageToView Index of the image to view (defaults to viewing first image on page if this parameter isn't set).
        */
        goToPage: function (page, imageToView) {
            if ($$.validPage(page)) {
                // Hide all thumbnail containers.
                $('.exposureThumbs li').removeClass('current').hide();

                $$.loadPage(page, imageToView);

                if (!$.exposure.imageControls) {
                    $$.updatePaging(page);
                }

                $$.currentPage = page;

                if ($.exposure.showControls) {
                    if ($.exposure.atFirstPage()) {
                        // Disable first page button.
                        if ($.exposure.controls.firstLast) {
                            $('.exposureFirstPage').addClass('disabled');
                        }

                        // Hide previous page button.
                        if (!$.exposure.loop && $.exposure.controls.prevNext) {
                            $('.exposurePrevPage').hide();
                        }
                    } else {
                        // Enable first page button.
                        if ($.exposure.controls.firstLast) {
                            $('.exposureFirstPage').removeClass('disabled');
                        }

                        // Show previous page button.
                        if (!$.exposure.loop && $.exposure.controls.prevNext) {
                            $('.exposurePrevPage').show();
                        }
                    }
                    if ($.exposure.atLastPage()) {
                        // Disable last page button.
                        if ($.exposure.controls.firstLast) {
                            $('.exposureLastPage').addClass('disabled');
                        }

                        // Hide next page button.
                        if (!$.exposure.loop && $.exposure.controls.prevNext) {
                            $('.exposureNextPage').hide();
                        }
                    } else {
                        // Enable last page button.
                        if ($.exposure.controls.firstLast) {
                            $('.exposureLastPage').removeClass('disabled');
                        }

                        // Show next page button.
                        if (!$.exposure.loop && $.exposure.controls.prevNext) {
                            $('.exposureNextPage').show();
                        }
                    }
                }

                // Page changed callback.
                if (!$.exposure.carouselControls) {
                    $.exposure.onPageChanged();
                }
            }
        },

        /**
        * View the first page.
        */
        firstPage: function () {
            if (!$.exposure.atFirstPage()) {
                $.exposure.goToPage(1);
            }
        },

        /**
        * View the last page.
        */
        lastPage: function () {
            if (!$.exposure.atLastPage()) {
                $.exposure.goToPage($.exposure.numberOfPages());
            }
        },

        /**
        * View the previous page.
        */
        prevPage: function () {
            if (!$.exposure.atFirstPage()) {
                // Go to previous page.
                $.exposure.goToPage($$.currentPage - 1);
            } else if ($.exposure.loop) {
                // At first page, go to last page.
                $.exposure.goToPage($.exposure.numberOfPages());
            }
        },

        /**
        * View the next page.
        */
        nextPage: function () {
            if (!$.exposure.atLastPage()) {
                // Go to next page.
                $.exposure.goToPage($$.currentPage + 1);
            } else if ($.exposure.loop) {
                // At last page, go back to first page.
                $.exposure.goToPage(1);
            }
        },

        /**
        * View a specific image.
        *
        * @param Index of image to view.
        */
        viewImage: function (index) {
            if ($$.current !== index) {
                if ($.exposure.enableSlideshow && !$$.slideshowTransition) {
                    $.exposure.pauseSlideshow();
                }
                var wrapper = $('.exposureWrapper');
                var validImage = false;
                var image = $$.images[index];
                if (image) {
                    var src = image.src;
                    var caption = image.caption;
                    var extraImageData = image.data;

                    if (src) {
                        validImage = true;

                        var hasThumb = $.exposure.showThumbs;
                        var thumb = null;
                        if ($.exposure.showThumbs) {
                            thumb = $('.exposureThumbs img[rel="' + index + '"]');
                            hasThumb = thumb && thumb.length;

                            // Light up active thumbnail.
                            if (hasThumb) {
                                thumb.parent().siblings().removeClass('active');
                                thumb.parent().addClass('active');
                            } else {
                                $('.exposureThumbs li.active').removeClass('active');
                            }
                        }

                        // Show loading animation.
                        wrapper.parent().removeClass('exposureLoaded');
                        if ($$.loaded(index)) {
                            // Hide loading animation if image already loaded.				
                            wrapper.parent().addClass('exposureLoaded');
                        }

                        var img = $$.loadImage(index, function () {
                            var lastImage = wrapper.find('.exposureImage');
                            if (lastImage.length) {
                                lastImage.removeClass('exposureCurrentImage');
                                lastImage.addClass('exposureLastImage');
                            }

                            $(this).addClass('exposureCurrentImage');

                            wrapper.append($(this));

                            // Enable browsing by clicking on the image.
                            if ($.exposure.clickingNavigation) {
                                $(this).click($.exposure.nextImage);
                            }

                            if (!$(this).width() || !$(this).height()) {
                                // Workaround for bug caused by AdBlock plugin for Chrome and Safari: 
                                // http://code.google.com/p/adblockforchrome/issues/detail?id=3701
                                var i = $(this);
                                var delay = setInterval(function () {
                                    $$.resizeContainer(i);
                                    clearTimeout(delay);
                                }, 2);
                            } else {
                                $$.resizeContainer($(this));
                            }

                            // Image is supposed to be viewed in full screen.
                            if ($.exposure.fullScreen && !$$.infullScreen) {
                                $.exposure.onEnterFullScreen($('.exposureMask'));
                                $$.infullScreen = true;
                            }

                            // Add caption and additional image data.							
                            var imageDataContainer = $.exposure.dataTarget ? $($.exposure.dataTarget) : wrapper.siblings('.exposureData');
                            if (imageDataContainer.length) {
                                if ($.exposure.showCaptions) {
                                    // Add caption to image data container.
                                    var captionContainer = imageDataContainer.find('.caption');
                                    if (captionContainer.length) {
                                        // Remove current caption from container.
                                        captionContainer.empty();
                                        if (!caption && hasThumb) {
                                            // Extract caption from thumbnail.
                                            caption = thumb.attr('title');
                                        }
                                    }
                                    captionContainer.html(caption);
                                }

                                if ($.exposure.showExtraData) {
                                    // Add extra image data to image data container.
                                    var extraImageDataContainer = imageDataContainer.find('.extra');
                                    if (extraImageDataContainer.length) {
                                        // Remove current data from container.
                                        extraImageDataContainer.empty();
                                        if (!extraImageData && hasThumb) {
                                            // Extract data from thumbnail.
                                            extraImageData = thumb.data('data');
                                        }
                                        extraImageDataContainer.html(extraImageData);
                                    }
                                }
                            }

                            // Image loaded callback.
                            $.exposure.onImage($(this), imageDataContainer, thumb);

                            // Preload next image.					
                            $$.preloadNextInQueue();
                        });
                    }
                }
                if (!validImage) {
                    wrapper.siblings().andSelf().empty();
                    $('.exposureThumbs li.active').removeClass('active');
                }

                if ($.exposure.imageControls) {
                    var page = $.exposure.pageNumberForImage(index);
                    if ($$.currentPage !== page && !$$.pageTransition) {
                        $.exposure.goToPage(page, index);
                    }
                    $$.updatePaging(index + 1);
                }

                $$.current = index;

                // If using carousel controls make sure to properly update the thumbnails.
                if ($.exposure.carouselControls && $$.images.length > $.exposure.pageSize) {
                    var firstVisibleImage = index;
                    var lastVisibleImage = $.exposure.pageSize - 1;
                    var flooredVisibleImages = Math.floor($.exposure.pageSize / 2);

                    if (!$.exposure.loop && index < flooredVisibleImages) {
                        firstVisibleImage = 0;
                    } else if (!$.exposure.loop && index >= ($$.images.length - flooredVisibleImages)) {
                        lastVisibleImage = $$.images.length - 1;
                        firstVisibleImage = lastVisibleImage - $.exposure.pageSize;
                    } else {
                        firstVisibleImage -= flooredVisibleImages;
                        lastVisibleImage = firstVisibleImage + $.exposure.pageSize - 1;
                    }

                    $.exposure.onCarousel(firstVisibleImage, lastVisibleImage);

                    $('.exposureThumbs li').removeClass('current').hide();
                    $$.viewThumbs(firstVisibleImage, lastVisibleImage);
                    $$.currentPage = $.exposure.pageNumberForImage(index);
                }
            }
        },

        /**
        * View first image.
        */
        firstImage: function () {
            if (!$.exposure.atFirstImage()) {
                if ($.exposure.separatePageBrowsing || $.exposure.atFirstPage()) {
                    $.exposure.viewImage(0);
                } else {
                    $.exposure.goToPage(1);
                }
            }
        },

        /**
        * View next image.
        */
        nextImage: function () {
            if (!$.exposure.separatePageBrowsing && $.exposure.lastImageOnPage()) {
                if ($.exposure.atLastPage() && $.exposure.loop) {
                    // At the last page, go back to first page.
                    $.exposure.goToPage(1);
                } else {
                    // Go to the next page.
                    $.exposure.goToPage($$.currentPage + 1);
                }
                // Next image callback.
                $.exposure.onNext();
            } else {
                var next = $.exposure.getNextImage();
                if (next !== null) {
                    // Select next image.
                    $.exposure.viewImage(next);
                    // Next image callback.
                    $.exposure.onNext();
                }
            }
            var nextNext = $.exposure.getNextImage();
            if (nextNext !== null) {
                // Add second next image to load queue.
                $$.addToLoadQueue(nextNext);
            }
        },

        /**
        * View previous image.
        */
        prevImage: function () {
            if (!$.exposure.separatePageBrowsing && $.exposure.firstImageOnPage()) {
                if ($.exposure.atFirstPage() && $.exposure.loop) {
                    // At the first page, go to the last page.
                    $.exposure.goToPage($.exposure.numberOfPages(), $.exposure.numberOfImages() - 1);
                } else {
                    // Go to the previous page.	
                    var page = $$.currentPage - 1;
                    $.exposure.goToPage(page, page * $.exposure.pageSize - 1);
                }
                // Previous image callback.
                $.exposure.onPrev();
            } else {
                var prev = $.exposure.getPrevImage();
                if (prev !== null) {
                    // Select next image.
                    $.exposure.viewImage(prev);
                    // Previous image callback.
                    $.exposure.onPrev();
                }
            }
            var prevPrev = $.exposure.getPrevImage();
            if (prevPrev !== null) {
                // Add second previous image to load queue.
                $$.addToLoadQueue(prevPrev);
            }
        },

        /**
        * View last image.
        */
        lastImage: function () {
            if (!$.exposure.atLastImage()) {
                if ($.exposure.separatePageBrowsing || $.exposure.atLastPage()) {
                    $.exposure.viewImage($.exposure.numberOfImages() - 1);
                } else {
                    $.exposure.goToPage($.exposure.numberOfPages(), $.exposure.numberOfImages() - 1);
                }
            }
        },

        /**
        * Play the slideshow.
        */
        playSlideshow: function () {
            if (!$$.playingSlideshow) {
                if ($.exposure.slideshowControlsTarget) {
                    $('.exposurePlaySlideshow').hide();
                    $('.exposurePauseSlideshow').show();
                }
                $$.slideshow();
                $$.playingSlideshow = true;
            }
            $.exposure.onSlideshowPlayed();
        },

        /**
        * Pause the slideshow.
        */
        pauseSlideshow: function () {
            if ($$.playingSlideshow) {
                if ($.exposure.slideshowControlsTarget) {
                    $('.exposurePlaySlideshow').show();
                    $('.exposurePauseSlideshow').hide();
                }
                $$.playingSlideshow = false;
                if ($$.slideshowTimer) {
                    clearTimeout($$.slideshowTimer);
                }
                $.exposure.onSlideshowPaused();
            }
        },

        /**
        * Toggle (play/pause)
        */
        toggleSlideshow: function () {
            if ($$.playingSlideshow) {
                $.exposure.pauseSlideshow();
            } else {
                $.exposure.playSlideshow();
            }
        },

        /**
        * Go to first image/page depending on imageControls setting.
        */
        first: function () {
            if ($.exposure.imageControls) {
                $.exposure.firstImage();
            } else {
                $.exposure.firstPage();
            }
        },

        /**
        * Go to previous image/page depending on imageControls setting.
        */
        prev: function () {
            if ($.exposure.imageControls) {
                $.exposure.prevImage();
            } else {
                $.exposure.prevPage();
            }
        },

        /**
        * Go to next image/page depending on imageControls setting.
        */
        next: function () {
            if ($.exposure.imageControls) {
                $.exposure.nextImage();
            } else {
                $.exposure.nextPage();
            }
        },

        /**
        * Go to last image/page depending on imageControls setting.
        */
        last: function () {
            if ($.exposure.imageControls) {
                $.exposure.lastImage();
            } else {
                $.exposure.lastPage();
            }
        },

        /**
        * Leave full screen mode.
        */
        exitFullScreen: function () {
            if ($$.infullScreen) {
                $.exposure.pauseSlideshow();
                $$.deselectCurrentImage();
                $.exposure.onExitFullScreen($('.exposureTarget'), $('.exposureMask'));
                $$.infullScreen = false;
            }
        },

        /**
        * Fit images to window (used in full screen mode).
        */
        fitToWindow: function () {
            $.exposure.maxWidth = $(window).width();
            $.exposure.maxHeight = $(window).height();
            var image = $('.exposureCurrentImage').width('auto').height('auto');
            $$.fitToMaxSize(image);

            if (!image.width() || !image.height()) {
                // Workaround for bug caused by AdBlock plugin for Chrome and Safari: 
                // http://code.google.com/p/adblockforchrome/issues/detail?id=3701
                var delay = setInterval(function () {
                    $$.centerImageInWindow(image);
                    clearTimeout(delay);
                }, 2);
            } else {
                $$.centerImageInWindow(image);
            }
        },

        /**
        * Default texts. Use the localization files to override these.
        */
        texts: {
            first: 'First',
            previous: 'Prev',
            next: 'Next',
            last: 'Last',
            play: 'Play slideshow',
            pause: 'Pause slideshow'
        }
    }
    });
})(jQuery);

(function(jQuery){jQuery.hotkeys={version:"0.8",specialKeys:{8:"backspace",9:"tab",13:"return",16:"shift",17:"ctrl",18:"alt",19:"pause",20:"capslock",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"insert",46:"del",96:"0",97:"1",98:"2",99:"3",100:"4",101:"5",102:"6",103:"7",104:"8",105:"9",106:"*",107:"+",109:"-",110:".",111:"/",112:"f1",113:"f2",114:"f3",115:"f4",116:"f5",117:"f6",118:"f7",119:"f8",120:"f9",121:"f10",122:"f11",123:"f12",144:"numlock",145:"scroll",191:"/",224:"meta"},shiftNums:{"`":"~","1":"!","2":"@","3":"#","4":"$","5":"%","6":"^","7":"&","8":"*","9":"(","0":")","-":"_","=":"+",";":": ","'":"\"",",":"<",".":">","/":"?","\\":"|"}};function keyHandler(handleObj){if(typeof handleObj.data!=="string"){return}var origHandler=handleObj.handler,keys=handleObj.data.toLowerCase().split(" ");handleObj.handler=function(event){if(this!==event.target&&(/textarea|select/i.test(event.target.nodeName)||event.target.type==="text")){return}var special=event.type!=="keypress"&&jQuery.hotkeys.specialKeys[event.which],character=String.fromCharCode(event.which).toLowerCase(),key,modif="",possible={};if(event.altKey&&special!=="alt"){modif+="alt+"}if(event.ctrlKey&&special!=="ctrl"){modif+="ctrl+"}if(event.metaKey&&!event.ctrlKey&&special!=="meta"){modif+="meta+"}if(event.shiftKey&&special!=="shift"){modif+="shift+"}if(special){possible[modif+special]=true}else{possible[modif+character]=true;possible[modif+jQuery.hotkeys.shiftNums[character]]=true;if(modif==="shift+"){possible[jQuery.hotkeys.shiftNums[character]]=true}}for(var i=0,l=keys.length;i<l;i++){if(possible[keys[i]]){return origHandler.apply(this,arguments)}}}}jQuery.each(["keydown","keyup","keypress"],function(){jQuery.event.special[this]={add:keyHandler}})})(jQuery);

