/**
 * 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',

    // 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);
