/**
 * 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.
  const allImages = {}
  let imageCounter = 0

  // Galleriffic static class
  $.galleriffic = {
    version: '2.0.1',

    // Strips invalid characters and any leading # characters
    normalizeHash(hash) {
      return hash.replace(/^.*#/, '').replace(/\?.*$/, '')
    },

    getImage(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(hash) {
      const imageData = $.galleriffic.getImage(hash)
      if (!imageData) return false

      const { gallery } = imageData
      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(hash, ownerGallery) {
      const imageData = $.galleriffic.getImage(hash)
      if (!imageData) return false

      const { gallery } = imageData
      if (ownerGallery && ownerGallery != gallery) return false

      return gallery.removeImageByIndex(imageData.index)
    }
  }

  const 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(e, link) {
        this.pause()

        if (!this.enableHistory) {
          // The href attribute holds the unique hash for an image
          const 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(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(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(listItem, thumbExists, insert, position) {
        const $li = (typeof listItem === 'string') ? $(listItem) : listItem
        const $aThumb = $li.find('a.thumb')
        const slideUrl = $aThumb.attr('href')
        const title = $aThumb.attr('title')
        const $caption = $li.find('.caption').remove()
        let 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

        const imageData = {
          title,
          slideUrl,
          caption: $caption,
          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)
        }

        const gallery = this

        // Add the element to the DOM
        if (!thumbExists) {
          // Update thumbs passing in addition post transition out handler
          this.updateThumbs(() => {
            const $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(index) {
        if (index < 0 || index >= this.data.length) return false

        const imageData = this.data[index]
        if (!imageData) return false

        this.removeImage(imageData)

        return true
      },

      // Convenience method that simply calls the global removeImageByHash method.
      removeImageByHash(hash) {
        return $.galleriffic.removeImageByHash(hash, this)
      },

      // Removes an image from the gallery.
      removeImage(imageData) {
        const { index } = imageData

        // 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(() => {
          const $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(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() {
        this.data = []
        const gallery = this

        this.find('ul.thumbs > li').each(function(i) {
          gallery.addImage($(this), true, false)
        })

        return this
      },

      isPreloadComplete: false,

      // Initalizes the image preloader
      preloadInit() {
        if (this.preloadAhead == 0) return this

        this.preloadStartIndex = this.currentImage.index
        const 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(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(startIndex, currentIndex) {
        // Check if startIndex has been relocated
        if (startIndex != this.preloadStartIndex) {
          const nextIndex = this.getNextIndex(this.preloadStartIndex)
          return this.preloadRecursive(this.preloadStartIndex, nextIndex)
        }

        const gallery = this

        // Now check for preloadAhead count
        let 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(() => { gallery.preloadRecursive(startIndex, currentIndex) }, 500)
          return this
        }

        const imageData = this.data[currentIndex]
        if (!imageData) return this

        // If already loaded, continue
        if (imageData.image) return this.preloadNext(startIndex, currentIndex)

        // Preload the image
        const 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(startIndex, currentIndex) {
        const nextIndex = this.getNextIndex(currentIndex)
        if (nextIndex == startIndex) {
          this.isPreloadComplete = true
        } else {
          // Use setTimeout to free up thread
          const gallery = this
          setTimeout(() => { 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(index) {
        let 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(index) {
        let prevIndex = index - 1
        if (prevIndex < 0) prevIndex = this.data.length - 1
        return prevIndex
      },

      // Pauses the slideshow
      pause() {
        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() {
        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) {
          const gallery = this
          this.slideshowTimeout = setTimeout(() => { gallery.ssAdvance() }, this.delay)
        }

        return this
      },

      // Toggles the state of the slideshow (playing/paused)
      toggleSlideshow() {
        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() {
        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(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(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(dontPause, bypassHistory) {
        const page = this.getCurrentPage()
        const lastPage = this.getNumPages() - 1
        if (page < lastPage) {
          const startIndex = page * this.numThumbs
          const 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(dontPause, bypassHistory) {
        const page = this.getCurrentPage()
        if (page > 0) {
          const startIndex = page * this.numThumbs
          const 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(index, dontPause, bypassHistory) {
        if (!dontPause) this.pause()

        if (index < 0) index = 0
        else if (index >= this.data.length) index = this.data.length - 1

        const 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(imageData) {
        const { index } = imageData

        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(isSync) {
        if (isSync) return this.defaultTransitionDuration
        return this.defaultTransitionDuration / 2
      },

      // Rebuilds the slideshow image and controls and performs transitions
      refresh() {
        const imageData = this.currentImage
        if (!imageData) return this

        const { index } = imageData

        // 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}`)
        }

        const previousSlide = this.$imageContainer.find('span.current').addClass('previous').removeClass('current')
        let 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
        const isSync = this.syncTransitions && imageData.image

        // Flag we are transitioning
        let isTransitioning = true
        const gallery = this

        const 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) {
          const 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(imageData, isSync) {
        const gallery = this
        const nextIndex = this.getNextIndex(imageData.index)

        // Construct new hidden span for the image
        const 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)
          })

        let 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(() => { gallery.ssAdvance() }, this.delay)
        }

        return this
      },

      // Returns the current page index that should be shown for the currentImage
      getCurrentPage() {
        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() {
        const page = this.getCurrentPage()
        if (page != this.displayedPage) this.updateThumbs()

        // Remove existing selected class and add selected class to new thumb
        const $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(postTransitionOutHandler) {
        const gallery = this
        const 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() {
        const needsPagination = this.data.length > this.numThumbs

        // Rebuild top pager
        if (this.enableTopPager) {
          let $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) {
          let $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)
        }

        const page = this.getCurrentPage()
        const startIndex = page * this.numThumbs
        let stopIndex = startIndex + this.numThumbs - 1
        if (stopIndex >= this.data.length) stopIndex = this.data.length - 1

        // Show/Hide thumbs
        const $thumbsUl = this.find('ul.thumbs')
        $thumbsUl.find('li').each(function(i) {
          const $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() {
        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(pager) {
        const gallery = this
        const numPages = this.getNumPages()
        const page = this.getCurrentPage()
        const startIndex = page * this.numThumbs
        let pagesRemaining = this.maxPagesToShow - 1

        let pageNum = page - Math.floor((this.maxPagesToShow - 1) / 2) + 1
        if (pageNum > 0) {
          const remainingPageCount = numPages - pageNum
          if (remainingPageCount < pagesRemaining) {
            pageNum -= (pagesRemaining - remainingPageCount)
          }
        }

        if (pageNum < 0) {
          pageNum = 0
        }

        // Prev Page Link
        if (page > 0) {
          const 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) {
          const lastPageNum = numPages - 1
          if (pageNum < lastPageNum) pager.append('<span class="ellipsis">&hellip;</span>')

          this.buildPageLink(pager, lastPageNum, numPages)
        }

        // Next Page Link
        const 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(pager, pageNum, numPages) {
        const pageLabel = pageNum + 1
        const currentPage = this.getCurrentPage()
        if (pageNum == currentPage) pager.append(`<span class="current">${pageLabel}</span>`)
        else if (pageNum < numPages) {
          const 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((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)
          })
      }
    }

    let initFirstImage = !this.enableHistory || !location.hash
    if (this.enableHistory && location.hash) {
      const hash = $.galleriffic.normalizeHash(location.hash)
      const 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((e) => {
        const 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(() => { gallery.preloadInit() }, 1000)

    return this
  }
}(jQuery))
