/**
 * jQuery Galleriffic plugin
 *
 * Copyright (c) 2008 Trent Foley (http://trentacular.com)
 * Licensed under the MIT License:
 *   http://www.opensource.org/licenses/mit-license.php
 *
 * Much thanks to primary contributer Ponticlaro (http://www.ponticlaro.com)
 */
;(function($) {
   // Globally keep track of all images by their unique hash.  Each item is an image data object.
   var allImages = {};
   var imageCounter = 0;

   // Galleriffic static class
   $.galleriffic = {
      version: '2.0.1',

      // Strips invalid characters and any leading # characters
      normalizeHash: function(hash) {
         return hash.replace(/^.*#/, '').replace(/\?.*$/, '');
      },

      getImage: function(hash) {
         if (!hash)
            return undefined;

         hash = $.galleriffic.normalizeHash(hash);
         return allImages[hash];
      },

      // Global function that looks up an image by its hash and displays the image.
      // Returns false when an image is not found for the specified hash.
      // @param {String} hash This is the unique hash value assigned to an image.
      gotoImage: function(hash) {
         var imageData = $.galleriffic.getImage(hash);
         if (!imageData)
            return false;

         var gallery = imageData.gallery;
         gallery.gotoImage(imageData);
         
         return true;
      },

      // Removes an image from its respective gallery by its hash.
      // Returns false when an image is not found for the specified hash or the
      // specified owner gallery does match the located images gallery.
      // @param {String} hash This is the unique hash value assigned to an image.
      // @param {Object} ownerGallery (Optional) When supplied, the located images
      // gallery is verified to be the same as the specified owning gallery before
      // performing the remove operation.
      removeImageByHash: function(hash, ownerGallery) {
         var imageData = $.galleriffic.getImage(hash);
         if (!imageData)
            return false;

         var gallery = imageData.gallery;
         if (ownerGallery && ownerGallery != gallery)
            return false;

         return gallery.removeImageByIndex(imageData.index);
      }
   };

   var defaults = {
      delay:                     3000,
      numThumbs:                 20,
      preloadAhead:              40, // Set to -1 to preload all images
      enableTopPager:            false,
      enableBottomPager:         true,
      maxPagesToShow:            7,
      imageContainerSel:         '',
      captionContainerSel:       '',
      controlsContainerSel:      '',
      loadingContainerSel:       '',
      renderSSControls:          true,
      renderNavControls:         true,
      playLinkText:              'Play',
      pauseLinkText:             'Pause',
      prevLinkText:              'Previous',
      nextLinkText:              'Next',
      nextPageLinkText:          'Next &rsaquo;',
      prevPageLinkText:          '&lsaquo; Prev',
      enableHistory:             false,
      enableKeyboardNavigation:  true,
      autoStart:                 false,
      syncTransitions:           false,
      defaultTransitionDuration: 1000,
      onSlideChange:             undefined, // accepts a delegate like such: function(prevIndex, nextIndex) { ... }
      onTransitionOut:           undefined, // accepts a delegate like such: function(slide, caption, isSync, callback) { ... }
      onTransitionIn:            undefined, // accepts a delegate like such: function(slide, caption, isSync) { ... }
      onPageTransitionOut:       undefined, // accepts a delegate like such: function(callback) { ... }
      onPageTransitionIn:        undefined, // accepts a delegate like such: function() { ... }
      onImageAdded:              undefined, // accepts a delegate like such: function(imageData, $li) { ... }
      onImageRemoved:            undefined  // accepts a delegate like such: function(imageData, $li) { ... }
   };

   // Primary Galleriffic initialization function that should be called on the thumbnail container.
   $.fn.galleriffic = function(settings) {
      //  Extend Gallery Object
      $.extend(this, {
         // Returns the version of the script
         version: $.galleriffic.version,

         // Current state of the slideshow
         isSlideshowRunning: false,
         slideshowTimeout: undefined,

         // This function is attached to the click event of generated hyperlinks within the gallery
         clickHandler: function(e, link) {
            this.pause();

            if (!this.enableHistory) {
               // The href attribute holds the unique hash for an image
               var hash = $.galleriffic.normalizeHash($(link).attr('href'));
               $.galleriffic.gotoImage(hash);
               e.preventDefault();
            }
         },

         // Appends an image to the end of the set of images.  Argument listItem can be either a jQuery DOM element or arbitrary html.
         // @param listItem Either a jQuery object or a string of html of the list item that is to be added to the gallery.
         appendImage: function(listItem) {
            this.addImage(listItem, false, false);
            return this;
         },

         // Inserts an image into the set of images.  Argument listItem can be either a jQuery DOM element or arbitrary html.
         // @param listItem Either a jQuery object or a string of html of the list item that is to be added to the gallery.
         // @param {Integer} position The index within the gallery where the item shouold be added.
         insertImage: function(listItem, position) {
            this.addImage(listItem, false, true, position);
            return this;
         },

         // Adds an image to the gallery and optionally inserts/appends it to the DOM (thumbExists)
         // @param listItem Either a jQuery object or a string of html of the list item that is to be added to the gallery.
         // @param {Boolean} thumbExists Specifies whether the thumbnail already exists in the DOM or if it needs to be added.
         // @param {Boolean} insert Specifies whether the the image is appended to the end or inserted into the gallery.
         // @param {Integer} position The index within the gallery where the item shouold be added.
         addImage: function(listItem, thumbExists, insert, position) {
            var $li = ( typeof listItem === "string" ) ? $(listItem) : listItem;          
            var $aThumb = $li.find('a.thumb');
            var slideUrl = $aThumb.attr('href');
            var title = $aThumb.attr('title');
            var $caption = $li.find('.caption').remove();
            var hash = $aThumb.attr('name');

            // Increment the image counter
            imageCounter++;

            // Autogenerate a hash value if none is present or if it is a duplicate
            if (!hash || allImages[''+hash]) {
               hash = imageCounter;
            }

            // Set position to end when not specified
            if (!insert)
               position = this.data.length;
            
            var imageData = {
               title:title,
               slideUrl:slideUrl,
               caption:$caption,
               hash:hash,
               gallery:this,
               index:position
            };

            // Add the imageData to this gallery's array of images
            if (insert) {
               this.data.splice(position, 0, imageData);

               // Reset index value on all imageData objects
               this.updateIndices(position);
            }
            else {
               this.data.push(imageData);
            }

            var gallery = this;

            // Add the element to the DOM
            if (!thumbExists) {
               // Update thumbs passing in addition post transition out handler
               this.updateThumbs(function() {
                  var $thumbsUl = gallery.find('ul.thumbs');
                  if (insert)
                     $thumbsUl.children(':eq('+position+')').before($li);
                  else
                     $thumbsUl.append($li);
                  
                  if (gallery.onImageAdded)
                     gallery.onImageAdded(imageData, $li);
               });
            }

            // Register the image globally
            allImages[''+hash] = imageData;

            // Setup attributes and click handler
            $aThumb.attr('rel', 'history')
               .attr('href', '#'+hash)
               .removeAttr('name')
               .click(function(e) {
                  gallery.clickHandler(e, this);
               });

            return this;
         },

         // Removes an image from the gallery based on its index.
         // Returns false when the index is out of range.
         removeImageByIndex: function(index) {
            if (index < 0 || index >= this.data.length)
               return false;
            
            var imageData = this.data[index];
            if (!imageData)
               return false;
            
            this.removeImage(imageData);
            
            return true;
         },

         // Convenience method that simply calls the global removeImageByHash method.
         removeImageByHash: function(hash) {
            return $.galleriffic.removeImageByHash(hash, this);
         },

         // Removes an image from the gallery.
         removeImage: function(imageData) {
            var index = imageData.index;
            
            // Remove the image from the gallery data array
            this.data.splice(index, 1);
            
            // Remove the global registration
            delete allImages[''+imageData.hash];
            
            // Remove the image's list item from the DOM
            this.updateThumbs(function() {
               var $li = gallery.find('ul.thumbs')
                  .children(':eq('+index+')')
                  .remove();

               if (gallery.onImageRemoved)
                  gallery.onImageRemoved(imageData, $li);
            });

            // Update each image objects index value
            this.updateIndices(index);

            return this;
         },

         // Updates the index values of the each of the images in the gallery after the specified index
         updateIndices: function(startIndex) {
            for (i = startIndex; i < this.data.length; i++) {
               this.data[i].index = i;
            }
            
            return this;
         },

         // Scraped the thumbnail container for thumbs and adds each to the gallery
         initializeThumbs: function() {
            this.data = [];
            var gallery = this;

            this.find('ul.thumbs > li').each(function(i) {
               gallery.addImage($(this), true, false);
            });

            return this;
         },

         isPreloadComplete: false,

         // Initalizes the image preloader
         preloadInit: function() {
            if (this.preloadAhead == 0) return this;
            
            this.preloadStartIndex = this.currentImage.index;
            var nextIndex = this.getNextIndex(this.preloadStartIndex);
            return this.preloadRecursive(this.preloadStartIndex, nextIndex);
         },

         // Changes the location in the gallery the preloader should work
         // @param {Integer} index The index of the image where the preloader should restart at.
         preloadRelocate: function(index) {
            // By changing this startIndex, the current preload script will restart
            this.preloadStartIndex = index;
            return this;
         },

         // Recursive function that performs the image preloading
         // @param {Integer} startIndex The index of the first image the current preloader started on.
         // @param {Integer} currentIndex The index of the current image to preload.
         preloadRecursive: function(startIndex, currentIndex) {
            // Check if startIndex has been relocated
            if (startIndex != this.preloadStartIndex) {
               var nextIndex = this.getNextIndex(this.preloadStartIndex);
               return this.preloadRecursive(this.preloadStartIndex, nextIndex);
            }

            var gallery = this;

            // Now check for preloadAhead count
            var preloadCount = currentIndex - startIndex;
            if (preloadCount < 0)
               preloadCount = this.data.length-1-startIndex+currentIndex;
            if (this.preloadAhead >= 0 && preloadCount > this.preloadAhead) {
               // Do this in order to keep checking for relocated start index
               setTimeout(function() { gallery.preloadRecursive(startIndex, currentIndex); }, 500);
               return this;
            }

            var imageData = this.data[currentIndex];
            if (!imageData)
               return this;

            // If already loaded, continue
            if (imageData.image)
               return this.preloadNext(startIndex, currentIndex); 
            
            // Preload the image
            var image = new Image();
            
            image.onload = function() {
               imageData.image = this;
               gallery.preloadNext(startIndex, currentIndex);
            };

            image.alt = imageData.title;
            image.src = imageData.slideUrl;

            return this;
         },
         
         // Called by preloadRecursive in order to preload the next image after the previous has loaded.
         // @param {Integer} startIndex The index of the first image the current preloader started on.
         // @param {Integer} currentIndex The index of the current image to preload.
         preloadNext: function(startIndex, currentIndex) {
            var nextIndex = this.getNextIndex(currentIndex);
            if (nextIndex == startIndex) {
               this.isPreloadComplete = true;
            } else {
               // Use setTimeout to free up thread
               var gallery = this;
               setTimeout(function() { gallery.preloadRecursive(startIndex, nextIndex); }, 100);
            }

            return this;
         },

         // Safe way to get the next image index relative to the current image.
         // If the current image is the last, returns 0
         getNextIndex: function(index) {
            var nextIndex = index+1;
            if (nextIndex >= this.data.length)
               nextIndex = 0;
            return nextIndex;
         },

         // Safe way to get the previous image index relative to the current image.
         // If the current image is the first, return the index of the last image in the gallery.
         getPrevIndex: function(index) {
            var prevIndex = index-1;
            if (prevIndex < 0)
               prevIndex = this.data.length-1;
            return prevIndex;
         },

         // Pauses the slideshow
         pause: function() {
            this.isSlideshowRunning = false;
            if (this.slideshowTimeout) {
               clearTimeout(this.slideshowTimeout);
               this.slideshowTimeout = undefined;
            }

            if (this.$controlsContainer) {
               this.$controlsContainer
                  .find('div.ss-controls a').removeClass().addClass('play')
                  .attr('title', this.playLinkText)
                  .attr('href', '#play')
                  .html(this.playLinkText);
            }
            
            return this;
         },

         // Plays the slideshow
         play: function() {
            this.isSlideshowRunning = true;

            if (this.$controlsContainer) {
               this.$controlsContainer
                  .find('div.ss-controls a').removeClass().addClass('pause')
                  .attr('title', this.pauseLinkText)
                  .attr('href', '#pause')
                  .html(this.pauseLinkText);
            }

            if (!this.slideshowTimeout) {
               var gallery = this;
               this.slideshowTimeout = setTimeout(function() { gallery.ssAdvance(); }, this.delay);
            }

            return this;
         },

         // Toggles the state of the slideshow (playing/paused)
         toggleSlideshow: function() {
            if (this.isSlideshowRunning)
               this.pause();
            else
               this.play();

            return this;
         },

         // Advances the slideshow to the next image and delegates navigation to the
         // history plugin when history is enabled
         // enableHistory is true
         ssAdvance: function() {
            if (this.isSlideshowRunning)
               this.next(true);

            return this;
         },

         // Advances the gallery to the next image.
         // @param {Boolean} dontPause Specifies whether to pause the slideshow.
         // @param {Boolean} bypassHistory Specifies whether to delegate navigation to the history plugin when history is enabled.  
         next: function(dontPause, bypassHistory) {
            this.gotoIndex(this.getNextIndex(this.currentImage.index), dontPause, bypassHistory);
            return this;
         },

         // Navigates to the previous image in the gallery.
         // @param {Boolean} dontPause Specifies whether to pause the slideshow.
         // @param {Boolean} bypassHistory Specifies whether to delegate navigation to the history plugin when history is enabled.
         previous: function(dontPause, bypassHistory) {
            this.gotoIndex(this.getPrevIndex(this.currentImage.index), dontPause, bypassHistory);
            return this;
         },

         // Navigates to the next page in the gallery.
         // @param {Boolean} dontPause Specifies whether to pause the slideshow.
         // @param {Boolean} bypassHistory Specifies whether to delegate navigation to the history plugin when history is enabled.
         nextPage: function(dontPause, bypassHistory) {
            var page = this.getCurrentPage();
            var lastPage = this.getNumPages() - 1;
            if (page < lastPage) {
               var startIndex = page * this.numThumbs;
               var nextPage = startIndex + this.numThumbs;
               this.gotoIndex(nextPage, dontPause, bypassHistory);
            }

            return this;
         },

         // Navigates to the previous page in the gallery.
         // @param {Boolean} dontPause Specifies whether to pause the slideshow.
         // @param {Boolean} bypassHistory Specifies whether to delegate navigation to the history plugin when history is enabled.
         previousPage: function(dontPause, bypassHistory) {
            var page = this.getCurrentPage();
            if (page > 0) {
               var startIndex = page * this.numThumbs;
               var prevPage = startIndex - this.numThumbs;           
               this.gotoIndex(prevPage, dontPause, bypassHistory);
            }
            
            return this;
         },

         // Navigates to the image at the specified index in the gallery
         // @param {Integer} index The index of the image in the gallery to display.
         // @param {Boolean} dontPause Specifies whether to pause the slideshow.
         // @param {Boolean} bypassHistory Specifies whether to delegate navigation to the history plugin when history is enabled.
         gotoIndex: function(index, dontPause, bypassHistory) {
            if (!dontPause)
               this.pause();
            
            if (index < 0) index = 0;
            else if (index >= this.data.length) index = this.data.length-1;
            
            var imageData = this.data[index];
            
            if (!bypassHistory && this.enableHistory)
               $.historyLoad(String(imageData.hash));  // At the moment, historyLoad only accepts string arguments
            else
               this.gotoImage(imageData);

            return this;
         },

         // This function is garaunteed to be called anytime a gallery slide changes.
         // @param {Object} imageData An object holding the image metadata of the image to navigate to.
         gotoImage: function(imageData) {
            var index = imageData.index;

            if (this.onSlideChange)
               this.onSlideChange(this.currentImage.index, index);
            
            this.currentImage = imageData;
            this.preloadRelocate(index);
            
            this.refresh();
            
            return this;
         },

         // Returns the default transition duration value.  The value is halved when not
         // performing a synchronized transition.
         // @param {Boolean} isSync Specifies whether the transitions are synchronized.
         getDefaultTransitionDuration: function(isSync) {
            if (isSync)
               return this.defaultTransitionDuration;
            return this.defaultTransitionDuration / 2;
         },

         // Rebuilds the slideshow image and controls and performs transitions
         refresh: function() {
            var imageData = this.currentImage;
            if (!imageData)
               return this;

            var index = imageData.index;

            // Update Controls
            if (this.$controlsContainer) {
               this.$controlsContainer
                  .find('div.nav-controls a.prev').attr('href', '#'+this.data[this.getPrevIndex(index)].hash).end()
                  .find('div.nav-controls a.next').attr('href', '#'+this.data[this.getNextIndex(index)].hash);
            }

            var previousSlide = this.$imageContainer.find('span.current').addClass('previous').removeClass('current');
            var previousCaption = 0;

            if (this.$captionContainer) {
               previousCaption = this.$captionContainer.find('span.current').addClass('previous').removeClass('current');
            }

            // Perform transitions simultaneously if syncTransitions is true and the next image is already preloaded
            var isSync = this.syncTransitions && imageData.image;

            // Flag we are transitioning
            var isTransitioning = true;
            var gallery = this;

            var transitionOutCallback = function() {
               // Flag that the transition has completed
               isTransitioning = false;

               // Remove the old slide
               previousSlide.remove();

               // Remove old caption
               if (previousCaption)
                  previousCaption.remove();

               if (!isSync) {
                  if (imageData.image && imageData.hash == gallery.data[gallery.currentImage.index].hash) {
                     gallery.buildImage(imageData, isSync);
                  } else {
                     // Show loading container
                     if (gallery.$loadingContainer) {
                        gallery.$loadingContainer.show();
                     }
                  }
               }
            };

            if (previousSlide.length == 0) {
               // For the first slide, the previous slide will be empty, so we will call the callback immediately
               transitionOutCallback();
            } else {
               if (this.onTransitionOut) {
                  this.onTransitionOut(previousSlide, previousCaption, isSync, transitionOutCallback);
               } else {
                  previousSlide.fadeTo(this.getDefaultTransitionDuration(isSync), 0.0, transitionOutCallback);
                  if (previousCaption)
                     previousCaption.fadeTo(this.getDefaultTransitionDuration(isSync), 0.0);
               }
            }

            // Go ahead and begin transitioning in of next image
            if (isSync)
               this.buildImage(imageData, isSync);

            if (!imageData.image) {
               var image = new Image();
               
               // Wire up mainImage onload event
               image.onload = function() {
                  imageData.image = this;

                  // Only build image if the out transition has completed and we are still on the same image hash
                  if (!isTransitioning && imageData.hash == gallery.data[gallery.currentImage.index].hash) {
                     gallery.buildImage(imageData, isSync);
                  }
               };

               // set alt and src
               image.alt = imageData.title;
               image.src = imageData.slideUrl;
            }

            // This causes the preloader (if still running) to relocate out from the currentIndex
            this.relocatePreload = true;

            return this.syncThumbs();
         },

         // Called by the refresh method after the previous image has been transitioned out or at the same time
         // as the out transition when performing a synchronous transition.
         // @param {Object} imageData An object holding the image metadata of the image to build.
         // @param {Boolean} isSync Specifies whether the transitions are synchronized.
         buildImage: function(imageData, isSync) {
            var gallery = this;
            var nextIndex = this.getNextIndex(imageData.index);

            // Construct new hidden span for the image
            var newSlide = this.$imageContainer
               .append('<span class="image-wrapper current"><a class="advance-link" rel="history" href="#'+this.data[nextIndex].hash+'" title="'+imageData.title+'">&nbsp;</a></span>')
               .find('span.current').css('opacity', '0');
  
            
            newSlide.find('a')
               .append(imageData.image)
               .click(function(e) {
                  gallery.clickHandler(e, this);
               });
            
            var newCaption = 0;
            if (this.$captionContainer) {
               // Construct new hidden caption for the image
               newCaption = this.$captionContainer
                  .append('<span class="image-caption current"></span>')
                  .find('span.current').css('opacity', '0')
                  .append(imageData.caption);
            }

            // Hide the loading conatiner
            if (this.$loadingContainer) {
               this.$loadingContainer.hide();
            }

            // Transition in the new image
            if (this.onTransitionIn) {
               this.onTransitionIn(newSlide, newCaption, isSync);
            } else {
               newSlide.fadeTo(this.getDefaultTransitionDuration(isSync), 1.0);
               if (newCaption)
                  newCaption.fadeTo(this.getDefaultTransitionDuration(isSync), 1.0);
            }
            
            if (this.isSlideshowRunning) {
               if (this.slideshowTimeout)
                  clearTimeout(this.slideshowTimeout);

               this.slideshowTimeout = setTimeout(function() { gallery.ssAdvance(); }, this.delay);
            }

            return this;
         },

         // Returns the current page index that should be shown for the currentImage
         getCurrentPage: function() {
            return Math.floor(this.currentImage.index / this.numThumbs);
         },

         // Applies the selected class to the current image's corresponding thumbnail.
         // Also checks if the current page has changed and updates the displayed page of thumbnails if necessary.
         syncThumbs: function() {
            var page = this.getCurrentPage();
            if (page != this.displayedPage)
               this.updateThumbs();

            // Remove existing selected class and add selected class to new thumb
            var $thumbs = this.find('ul.thumbs').children();
            $thumbs.filter('.selected').removeClass('selected');
            $thumbs.eq(this.currentImage.index).addClass('selected');

            return this;
         },

         // Performs transitions on the thumbnails container and updates the set of
         // thumbnails that are to be displayed and the navigation controls.
         // @param {Delegate} postTransitionOutHandler An optional delegate that is called after
         // the thumbnails container has transitioned out and before the thumbnails are rebuilt.
         updateThumbs: function(postTransitionOutHandler) {
            var gallery = this;
            var transitionOutCallback = function() {
               // Call the Post-transition Out Handler
               if (postTransitionOutHandler)
                  postTransitionOutHandler();
               
               gallery.rebuildThumbs();

               // Transition In the thumbsContainer
               if (gallery.onPageTransitionIn)
                  gallery.onPageTransitionIn();
               else
                  gallery.show();
            };

            // Transition Out the thumbsContainer
            if (this.onPageTransitionOut) {
               this.onPageTransitionOut(transitionOutCallback);
            } else {
               this.hide();
               transitionOutCallback();
            }

            return this;
         },

         // Updates the set of thumbnails that are to be displayed and the navigation controls.
         rebuildThumbs: function() {
            var needsPagination = this.data.length > this.numThumbs;

            // Rebuild top pager
            if (this.enableTopPager) {
               var $topPager = this.find('div.top');
               if ($topPager.length == 0)
                  $topPager = this.prepend('<div class="top pagination"></div>').find('div.top');
               else
                  $topPager.empty();

               if (needsPagination)
                  this.buildPager($topPager);
            }

            // Rebuild bottom pager
            if (this.enableBottomPager) {
               var $bottomPager = this.find('div.bottom');
               if ($bottomPager.length == 0)
                  $bottomPager = this.append('<div class="bottom pagination"></div>').find('div.bottom');
               else
                  $bottomPager.empty();

               if (needsPagination)
                  this.buildPager($bottomPager);
            }

            var page = this.getCurrentPage();
            var startIndex = page*this.numThumbs;
            var stopIndex = startIndex+this.numThumbs-1;
            if (stopIndex >= this.data.length)
               stopIndex = this.data.length-1;

            // Show/Hide thumbs
            var $thumbsUl = this.find('ul.thumbs');
            $thumbsUl.find('li').each(function(i) {
               var $li = $(this);
               if (i >= startIndex && i <= stopIndex) {
                  $li.show();
               } else {
                  $li.hide();
               }
            });

            this.displayedPage = page;

            // Remove the noscript class from the thumbs container ul
            $thumbsUl.removeClass('noscript');
            
            return this;
         },

         // Returns the total number of pages required to display all the thumbnails.
         getNumPages: function() {
            return Math.ceil(this.data.length/this.numThumbs);
         },

         // Rebuilds the pager control in the specified matched element.
         // @param {jQuery} pager A jQuery element set matching the particular pager to be rebuilt.
         buildPager: function(pager) {
            var gallery = this;
            var numPages = this.getNumPages();
            var page = this.getCurrentPage();
            var startIndex = page * this.numThumbs;
            var pagesRemaining = this.maxPagesToShow - 1;
            
            var pageNum = page - Math.floor((this.maxPagesToShow - 1) / 2) + 1;
            if (pageNum > 0) {
               var remainingPageCount = numPages - pageNum;
               if (remainingPageCount < pagesRemaining) {
                  pageNum = pageNum - (pagesRemaining - remainingPageCount);
               }
            }

            if (pageNum < 0) {
               pageNum = 0;
            }

            // Prev Page Link
            if (page > 0) {
               var prevPage = startIndex - this.numThumbs;
               pager.append('<a rel="history" href="#'+this.data[prevPage].hash+'" title="'+this.prevPageLinkText+'">'+this.prevPageLinkText+'</a>');
            }

            // Create First Page link if needed
            if (pageNum > 0) {
               this.buildPageLink(pager, 0, numPages);
               if (pageNum > 1)
                  pager.append('<span class="ellipsis">&hellip;</span>');
               
               pagesRemaining--;
            }

            // Page Index Links
            while (pagesRemaining > 0) {
               this.buildPageLink(pager, pageNum, numPages);
               pagesRemaining--;
               pageNum++;
            }

            // Create Last Page link if needed
            if (pageNum < numPages) {
               var lastPageNum = numPages - 1;
               if (pageNum < lastPageNum)
                  pager.append('<span class="ellipsis">&hellip;</span>');

               this.buildPageLink(pager, lastPageNum, numPages);
            }

            // Next Page Link
            var nextPage = startIndex + this.numThumbs;
            if (nextPage < this.data.length) {
               pager.append('<a rel="history" href="#'+this.data[nextPage].hash+'" title="'+this.nextPageLinkText+'">'+this.nextPageLinkText+'</a>');
            }

            pager.find('a').click(function(e) {
               gallery.clickHandler(e, this);
            });

            return this;
         },

         // Builds a single page link within a pager.  This function is called by buildPager
         // @param {jQuery} pager A jQuery element set matching the particular pager to be rebuilt.
         // @param {Integer} pageNum The page number of the page link to build.
         // @param {Integer} numPages The total number of pages required to display all thumbnails.
         buildPageLink: function(pager, pageNum, numPages) {
            var pageLabel = pageNum + 1;
            var currentPage = this.getCurrentPage();
            if (pageNum == currentPage)
               pager.append('<span class="current">'+pageLabel+'</span>');
            else if (pageNum < numPages) {
               var imageIndex = pageNum*this.numThumbs;
               pager.append('<a rel="history" href="#'+this.data[imageIndex].hash+'" title="'+pageLabel+'">'+pageLabel+'</a>');
            }
            
            return this;
         }
      });

      // Now initialize the gallery
      $.extend(this, defaults, settings);
      
      // Verify the history plugin is available
      if (this.enableHistory && !$.historyInit)
         this.enableHistory = false;
      
      // Select containers
      if (this.imageContainerSel) this.$imageContainer = $(this.imageContainerSel);
      if (this.captionContainerSel) this.$captionContainer = $(this.captionContainerSel);
      if (this.loadingContainerSel) this.$loadingContainer = $(this.loadingContainerSel);

      // Initialize the thumbails
      this.initializeThumbs();
      
      if (this.maxPagesToShow < 3)
         this.maxPagesToShow = 3;

      this.displayedPage = -1;
      this.currentImage = this.data[0];
      var gallery = this;

      // Hide the loadingContainer
      if (this.$loadingContainer)
         this.$loadingContainer.hide();

      // Setup controls
      if (this.controlsContainerSel) {
         this.$controlsContainer = $(this.controlsContainerSel).empty();
         
         if (this.renderSSControls) {
            if (this.autoStart) {
               this.$controlsContainer
                  .append('<div class="ss-controls"><a href="#pause" class="pause" title="'+this.pauseLinkText+'">'+this.pauseLinkText+'</a></div>');
            } else {
               this.$controlsContainer
                  .append('<div class="ss-controls"><a href="#play" class="play" title="'+this.playLinkText+'">'+this.playLinkText+'</a></div>');
            }

            this.$controlsContainer.find('div.ss-controls a')
               .click(function(e) {
                  gallery.toggleSlideshow();
                  e.preventDefault();
                  return false;
               });
         }
      
         if (this.renderNavControls) {
            this.$controlsContainer
               .append('<div class="nav-controls"><a class="prev" rel="history" title="'+this.prevLinkText+'">'+this.prevLinkText+'</a><a class="next" rel="history" title="'+this.nextLinkText+'">'+this.nextLinkText+'</a></div>')
               .find('div.nav-controls a')
               .click(function(e) {
                  gallery.clickHandler(e, this);
               });
         }
      }

      var initFirstImage = !this.enableHistory || !location.hash;
      if (this.enableHistory && location.hash) {
         var hash = $.galleriffic.normalizeHash(location.hash);
         var imageData = allImages[hash];
         if (!imageData)
            initFirstImage = true;
      }

      // Setup gallery to show the first image
      if (initFirstImage)
         this.gotoIndex(0, false, true);

      // Setup Keyboard Navigation
      if (this.enableKeyboardNavigation) {
         $(document).keydown(function(e) {
            var key = e.charCode ? e.charCode : e.keyCode ? e.keyCode : 0;
            switch(key) {
               case 32: // space
                  gallery.next();
                  e.preventDefault();
                  break;
               case 33: // Page Up
                  gallery.previousPage();
                  e.preventDefault();
                  break;
               case 34: // Page Down
                  gallery.nextPage();
                  e.preventDefault();
                  break;
               case 35: // End
                  gallery.gotoIndex(gallery.data.length-1);
                  e.preventDefault();
                  break;
               case 36: // Home
                  gallery.gotoIndex(0);
                  e.preventDefault();
                  break;
               case 37: // left arrow
                  gallery.previous();
                  e.preventDefault();
                  break;
               case 39: // right arrow
                  gallery.next();
                  e.preventDefault();
                  break;
            }
         });
      }

      // Auto start the slideshow
      if (this.autoStart)
         this.play();

      // Kickoff Image Preloader after 1 second
      setTimeout(function() { gallery.preloadInit(); }, 1000);

      return this;
   };
})(jQuery);

