/*
 * Backstretch
 * http://srobbin.com/jquery-plugins/backstretch/
 *
 * Copyright (c) 2013 Scott Robbin
 * Licensed under the MIT license.
 */

;(function ($, window, undefined) {
  'use strict';

  /** @const */
  var YOUTUBE_REGEXP = /^.*(youtu\.be\/|youtube\.com\/v\/|youtube\.com\/embed\/|youtube\.com\/watch\?v=|youtube\.com\/watch\?.*\&v=)([^#\&\?]*).*/i;

  /* PLUGIN DEFINITION
   * ========================= */

  $.fn.backstretch = function (images, options) {
    var args = arguments;

    /*
     * Scroll the page one pixel to get the right window height on iOS
     * Pretty harmless for everyone else
    */
    if ($(window).scrollTop() === 0) {
      window.scrollTo(0, 0);
    }

    var returnValues;

    this.each(function (eachIndex) {
      var $this = $(this)
        , obj = $this.data('backstretch');

      // Do we already have an instance attached to this element?
      if (obj) {

        // Is this a method they're trying to execute?
        if (typeof args[0] === 'string' &&
          typeof obj[args[0]] === 'function') {

          // Call the method
          var returnValue = obj[args[0]].apply(obj, Array.prototype.slice.call(args, 1));
          if (returnValue === obj) { // If a method is chaining
            returnValue = undefined;
          }
          if (returnValue !== undefined) {
            returnValues = returnValues || [];
            returnValues[eachIndex] = returnValue;
          }

          return; // Nothing further to do
        }

        // Merge the old options with the new
        options = $.extend(obj.options, options);

        // Remove the old instance
        if (typeof obj === 'object' && 'destroy' in obj) {
          obj.destroy(true);
        }
      }

      // We need at least one image
      if (!images || (images && images.length === 0)) {
        var cssBackgroundImage = $this.css('background-image');
        if (cssBackgroundImage && cssBackgroundImage !== 'none') {
          images = [{url: $this.css('backgroundImage').replace(/url\(|\)|"|'/g, "")}];
        }
        else {
          $.error('No images were supplied for Backstretch, or element must have a CSS-defined background image.');
        }
      }

      obj = new Backstretch(this, images, options || {});
      $this.data('backstretch', obj);
    });

    return returnValues ? returnValues.length === 1 ? returnValues[0] : returnValues : this;
  };

  // If no element is supplied, we'll attach to body
  $.backstretch = function (images, options) {
    // Return the instance
    return $('body')
      .backstretch(images, options)
      .data('backstretch');
  };

  // Custom selector
  $.expr[':'].backstretch = function (elem) {
    return $(elem).data('backstretch') !== undefined;
  };

  /* DEFAULTS
   * ========================= */

  $.fn.backstretch.defaults = {
    duration: 5000                // Amount of time in between slides (if slideshow)
    , transition: 'fade'          // Type of transition between slides
    , transitionDuration: 0       // Duration of transition between slides
    , animateFirst: true          // Animate the transition of first image of slideshow in?
    , alignX: 0.5                 // The x-alignment for the image, can be 'left'|'center'|'right' or any number between 0.0 and 1.0
    , alignY: 0.5                 // The y-alignment for the image, can be 'top'|'center'|'bottom' or any number between 0.0 and 1.0
    , paused: false               // Whether the images should slide after given duration
    , start: 0                    // Index of the first image to show
    , preload: 2                  // How many images preload at a time?
    , preloadSize: 1              // How many images can we preload in parallel?
    , resolutionRefreshRate: 2500 // How long to wait before switching resolution?
    , resolutionChangeRatioThreshold: 0.1 // How much a change should it be before switching resolution?
  };

  /* STYLES
   *
   * Baked-in styles that we'll apply to our elements.
   * In an effort to keep the plugin simple, these are not exposed as options.
   * That said, anyone can override these in their own stylesheet.
   * ========================= */
  var styles = {
    wrap: {
      left: 0
      , top: 0
      , overflow: 'hidden'
      , margin: 0
      , padding: 0
      , height: '100%'
      , width: '100%'
      , zIndex: -999999
    }
    , itemWrapper: {
      position: 'absolute'
      , display: 'none'
      , margin: 0
      , padding: 0
      , border: 'none'
      , width: '100%'
      , height: '100%'
      , zIndex: -999999
    }
    , item: {
      position: 'absolute'
      , margin: 0
      , padding: 0
      , border: 'none'
      , width: '100%'
      , height: '100%'
      , maxWidth: 'none'
    }
  };

  /* Given an array of different options for an image,
   * choose the optimal image for the container size.
   *
   * Given an image template (a string with {{ width }} and/or
   * {{height}} inside) and a container object, returns the
   * image url with the exact values for the size of that
   * container.
   *
   * Returns an array of urls optimized for the specified resolution.
   *
   */
  var optimalSizeImages = (function () {

    /* Sorts the array of image sizes based on width */
    var widthInsertSort = function (arr) {
      for (var i = 1; i < arr.length; i++) {
        var tmp = arr[i],
          j = i;
        while (arr[j - 1] && parseInt(arr[j - 1].width, 10) > parseInt(tmp.width, 10)) {
          arr[j] = arr[j - 1];
          --j;
        }
        arr[j] = tmp;
      }

      return arr;
    };

    /* Given an array of various sizes of the same image and a container width,
     * return the best image.
     */
    var selectBest = function (containerWidth, containerHeight, imageSizes) {

      var devicePixelRatio = window.devicePixelRatio || 1;
      var deviceOrientation = getDeviceOrientation();
      var windowOrientation = getWindowOrientation();
      var wrapperOrientation = (containerHeight > containerWidth) ?
        'portrait' :
        (containerWidth > containerHeight ? 'landscape' : 'square');

      var lastAllowedImage = 0;
      var testWidth;

      for (var j = 0, image; j < imageSizes.length; j++) {

        image = imageSizes[j];

        // In case a new image was pushed in, process it:
        if (typeof image === 'string') {
          image = imageSizes[j] = {url: image};
        }

        if (image.pixelRatio && image.pixelRatio !== 'auto' && parseFloat(image.pixelRatio) !== devicePixelRatio) {
          // We disallowed choosing this image for current device pixel ratio,
          // So skip this one.
          continue;
        }

        if (image.deviceOrientation && image.deviceOrientation !== deviceOrientation) {
          // We disallowed choosing this image for current device orientation,
          // So skip this one.
          continue;
        }

        if (image.windowOrientation && image.windowOrientation !== deviceOrientation) {
          // We disallowed choosing this image for current window orientation,
          // So skip this one.
          continue;
        }

        if (image.orientation && image.orientation !== wrapperOrientation) {
          // We disallowed choosing this image for current element's orientation,
          // So skip this one.
          continue;
        }

        // Mark this one as the last one we investigated
        // which does not violate device pixel ratio rules.
        // We may choose this one later if there's no match.
        lastAllowedImage = j;

        // For most images, we match the specified width against element width,
        // And enforcing a limit depending on the "pixelRatio" property if specified.
        // But if a pixelRatio="auto", then we consider the width as the physical width of the image,
        // And match it while considering the device's pixel ratio.
        testWidth = containerWidth;
        if (image.pixelRatio === 'auto') {
          containerWidth *= devicePixelRatio;
        }

        // Stop when the width of the image is larger or equal to the container width
        if (image.width >= testWidth) {
          break;
        }
      }

      // Use the image located at where we stopped
      return imageSizes[Math.min(j, lastAllowedImage)];
    };

    var replaceTagsInUrl = function (url, templateReplacer) {

      if (typeof url === 'string') {
        url = url.replace(/{{(width|height)}}/g, templateReplacer);
      }
      else if (url instanceof Array) {
        for (var i = 0; i < url.length; i++) {
          if (url[i].src) {
            url[i].src = replaceTagsInUrl(url[i].src, templateReplacer);
          }
          else {
            url[i] = replaceTagsInUrl(url[i], templateReplacer);
          }
        }
      }

      return url;
    };

    return function ($container, images) {
      var containerWidth = $container.width(),
        containerHeight = $container.height();

      var chosenImages = [];

      var templateReplacer = function (match, key) {
        if (key === 'width') {
          return containerWidth;
        }
        if (key === 'height') {
          return containerHeight;
        }
        return match;
      };

      for (var i = 0; i < images.length; i++) {
        if ($.isArray(images[i])) {
          images[i] = widthInsertSort(images[i]);
          var chosen = selectBest(containerWidth, containerHeight, images[i]);
          chosenImages.push(chosen);
        }
        else {
          // In case a new image was pushed in, process it:
          if (typeof images[i] === 'string') {
            images[i] = {url: images[i]};
          }

          var item = $.extend({}, images[i]);
          item.url = replaceTagsInUrl(item.url, templateReplacer);
          chosenImages.push(item);
        }
      }
      return chosenImages;
    };

  })();

  var isVideoSource = function (source) {
    return YOUTUBE_REGEXP.test(source.url) || source.isVideo;
  };

  /* Preload images */
  var preload = (function (sources, startAt, count, batchSize, callback) {
    // Plugin cache
    var cache = [];

    // Wrapper for cache
    var caching = function (image) {
      for (var i = 0; i < cache.length; i++) {
        if (cache[i].src === image.src) {
          return cache[i];
        }
      }
      cache.push(image);
      return image;
    };

    // Execute callback
    var exec = function (sources, callback, last) {
      if (typeof callback === 'function') {
        callback.call(sources, last);
      }
    };

    // Closure to hide cache
    return function preload(sources, startAt, count, batchSize, callback) {
      // Check input data
      if (typeof sources === 'undefined') {
        return;
      }
      if (!$.isArray(sources)) {
        sources = [sources];
      }

      if (arguments.length < 5 && typeof arguments[arguments.length - 1] === 'function') {
        callback = arguments[arguments.length - 1];
      }

      startAt = (typeof startAt === 'function' || !startAt) ? 0 : startAt;
      count = (typeof count === 'function' || !count || count < 0) ? sources.length : Math.min(count, sources.length);
      batchSize = (typeof batchSize === 'function' || !batchSize) ? 1 : batchSize;

      if (startAt >= sources.length) {
        startAt = 0;
        count = 0;
      }
      if (batchSize < 0) {
        batchSize = count;
      }
      batchSize = Math.min(batchSize, count);

      var next = sources.slice(startAt + batchSize, count - batchSize);
      sources = sources.slice(startAt, batchSize);
      count = sources.length;

      // If sources array is empty
      if (!count) {
        exec(sources, callback, true);
        return;
      }

      // Image loading callback
      var countLoaded = 0;

      var loaded = function () {
        countLoaded++;
        if (countLoaded !== count) {
          return;
        }

        exec(sources, callback, !next);
        preload(next, 0, 0, batchSize, callback);
      };

      // Loop sources to preload
      var image;

      for (var i = 0; i < sources.length; i++) {

        if (isVideoSource(sources[i])) {

          // Do not preload videos. There are issues with that.
          // First - we need to keep an instance of the preloaded and use that exactly, not a copy.
          // Second - there are memory issues.
          // If there will be a requirement from users - I'll try to implement this.

          continue;

        }
        else {

          image = new Image();
          image.src = sources[i].url;

          image = caching(image);

          if (image.complete) {
            loaded();
          }
          else {
            $(image).on('load error', loaded);
          }

        }

      }
    };
  })();

  /* Process images array */
  var processImagesArray = function (images) {
    var processed = [];
    for (var i = 0; i < images.length; i++) {
      if (typeof images[i] === 'string') {
        processed.push({url: images[i]});
      }
      else if ($.isArray(images[i])) {
        processed.push(processImagesArray(images[i]));
      }
      else {
        processed.push(processOptions(images[i]));
      }
    }
    return processed;
  };

  /* Process options */
  var processOptions = function (options, required) {

    // Convert old options

    // centeredX/centeredY are deprecated
    if (options.centeredX || options.centeredY) {
      if (window.console && window.console.log) {
        window.console.log('jquery.backstretch: `centeredX`/`centeredY` is deprecated, please use `alignX`/`alignY`');
      }
      if (options.centeredX) {
        options.alignX = 0.5;
      }
      if (options.centeredY) {
        options.alignY = 0.5;
      }
    }

    // Deprecated spec
    if (options.speed !== undefined) {

      if (window.console && window.console.log) {
        window.console.log('jquery.backstretch: `speed` is deprecated, please use `transitionDuration`');
      }

      options.transitionDuration = options.speed;
      options.transition = 'fade';
    }

    // Typo
    if (options.resolutionChangeRatioTreshold !== undefined) {
      window.console.log('jquery.backstretch: `treshold` is a typo!');
      options.resolutionChangeRatioThreshold = options.resolutionChangeRatioTreshold;
    }

    // Current spec that needs processing

    if (options.fadeFirst !== undefined) {
      options.animateFirst = options.fadeFirst;
    }

    if (options.fade !== undefined) {
      options.transitionDuration = options.fade;
      options.transition = 'fade';
    }

    if (options.scale) {
      options.scale = validScale(options.scale);
    }

    return processAlignOptions(options);
  };

  /* Process align options */
  var processAlignOptions = function (options, required) {
    if (options.alignX === 'left') {
      options.alignX = 0.0;
    }
    else if (options.alignX === 'center') {
      options.alignX = 0.5;
    }
    else if (options.alignX === 'right') {
      options.alignX = 1.0;
    }
    else {
      if (options.alignX !== undefined || required) {
        options.alignX = parseFloat(options.alignX);
        if (isNaN(options.alignX)) {
          options.alignX = 0.5;
        }
      }
    }

    if (options.alignY === 'top') {
      options.alignY = 0.0;
    }
    else if (options.alignY === 'center') {
      options.alignY = 0.5;
    }
    else if (options.alignY === 'bottom') {
      options.alignY = 1.0;
    }
    else {
      if (options.alignX !== undefined || required) {
        options.alignY = parseFloat(options.alignY);
        if (isNaN(options.alignY)) {
          options.alignY = 0.5;
        }
      }
    }

    return options;
  };

  var SUPPORTED_SCALE_OPTIONS = {
    'cover': 'cover',
    'fit': 'fit',
    'fit-smaller': 'fit-smaller',
    'fill': 'fill'
  };

  function validScale(scale) {
    if (!SUPPORTED_SCALE_OPTIONS.hasOwnProperty(scale)) {
      return 'cover';
    }
    return scale;
  }

  /* CLASS DEFINITION
   * ========================= */
  var Backstretch = function (container, images, options) {
    this.options = $.extend({}, $.fn.backstretch.defaults, options || {});

    this.firstShow = true;

    // Process options
    processOptions(this.options, true);

    /* In its simplest form, we allow Backstretch to be called on an image path.
     * e.g. $.backstretch('/path/to/image.jpg')
     * So, we need to turn this back into an array.
     */
    this.images = processImagesArray($.isArray(images) ? images : [images]);

    /**
     * Paused-Option
     */
    if (this.options.paused) {
      this.paused = true;
    }

    /**
     * Start-Option (Index)
     */
    if (this.options.start >= this.images.length) {
      this.options.start = this.images.length - 1;
    }
    if (this.options.start < 0) {
      this.options.start = 0;
    }

    // Convenience reference to know if the container is body.
    this.isBody = container === document.body;

    /* We're keeping track of a few different elements
     *
     * Container: the element that Backstretch was called on.
     * Wrap: a DIV that we place the image into, so we can hide the overflow.
     * Root: Convenience reference to help calculate the correct height.
     */
    var $window = $(window);
    this.$container = $(container);
    this.$root = this.isBody ? supportsFixedPosition ? $window : $(document) : this.$container;

    this.originalImages = this.images;
    this.images = optimalSizeImages(
      this.options.alwaysTestWindowResolution ? $window : this.$root,
      this.originalImages);

    /**
     * Pre-Loading.
     * This is the first image, so we will preload a minimum of 1 images.
     */
    preload(this.images, this.options.start || 0, this.options.preload || 1);

    // Don't create a new wrap if one already exists (from a previous instance of Backstretch)
    var $existing = this.$container.children(".backstretch").first();
    this.$wrap = $existing.length ? $existing :
      $('<div class="backstretch"></div>')
        .css(this.options.bypassCss ? {} : styles.wrap)
        .appendTo(this.$container);

    if (!this.options.bypassCss) {

      // Non-body elements need some style adjustments
      if (!this.isBody) {
        // If the container is statically positioned, we need to make it relative,
        // and if no zIndex is defined, we should set it to zero.
        var position = this.$container.css('position')
          , zIndex = this.$container.css('zIndex');

        this.$container.css({
          position: position === 'static' ? 'relative' : position
          , zIndex: zIndex === 'auto' ? 0 : zIndex
        });

        // Needs a higher z-index
        this.$wrap.css({zIndex: -999998});
      }

      // Fixed or absolute positioning?
      this.$wrap.css({
        position: this.isBody && supportsFixedPosition ? 'fixed' : 'absolute'
      });

    }

    // Set the first image
    this.index = this.options.start;
    this.show(this.index);

    // Listen for resize
    $window.on('resize.backstretch', $.proxy(this.resize, this))
           .on('orientationchange.backstretch', $.proxy(function () {
             // Need to do this in order to get the right window height
             if (this.isBody && window.pageYOffset === 0) {
               window.scrollTo(0, 1);
               this.resize();
             }
           }, this));
  };

  var performTransition = function (options) {

    var transition = options.transition || 'fade';

    // Look for multiple options
    if (typeof transition === 'string' && transition.indexOf('|') > -1) {
      transition = transition.split('|');
    }

    if (transition instanceof Array) {
      transition = transition[Math.round(Math.random() * (transition.length - 1))];
    }

    var $new = options['new'];
    var $old = options['old'] ? options['old'] : $([]);

    switch (transition.toString().toLowerCase()) {

      default:
      case 'fade':
        $new.fadeIn({
          duration: options.duration,
          complete: options.complete,
          easing: options.easing || undefined
        });
        break;

      case 'fadeinout':
      case 'fade_in_out':

        var fadeInNew = function () {
          $new.fadeIn({
            duration: options.duration / 2,
            complete: options.complete,
            easing: options.easing || undefined
          });
        };

        if ($old.length) {
          $old.fadeOut({
            duration: options.duration / 2,
            complete: fadeInNew,
            easing: options.easing || undefined
          });
        }
        else {
          fadeInNew();
        }

        break;

      case 'pushleft':
      case 'push_left':
      case 'pushright':
      case 'push_right':
      case 'pushup':
      case 'push_up':
      case 'pushdown':
      case 'push_down':
      case 'coverleft':
      case 'cover_left':
      case 'coverright':
      case 'cover_right':
      case 'coverup':
      case 'cover_up':
      case 'coverdown':
      case 'cover_down':

        var transitionParts = transition.match(/^(cover|push)_?(.*)$/);

        var animProp = transitionParts[2] === 'left' ? 'right' :
          transitionParts[2] === 'right' ? 'left' :
            transitionParts[2] === 'down' ? 'top' :
              transitionParts[2] === 'up' ? 'bottom' :
                'right';

        var newCssStart = {
          'display': ''
        }, newCssAnim = {};
        newCssStart[animProp] = '-100%';
        newCssAnim[animProp] = 0;

        $new
          .css(newCssStart)
          .animate(newCssAnim, {
            duration: options.duration,
            complete: function () {
              $new.css(animProp, '');
              options.complete.apply(this, arguments);
            },
            easing: options.easing || undefined
          });

        if (transitionParts[1] === 'push' && $old.length) {
          var oldCssAnim = {};
          oldCssAnim[animProp] = '100%';

          $old
            .animate(oldCssAnim, {
              duration: options.duration,
              complete: function () {
                $old.css('display', 'none');
              },
              easing: options.easing || undefined
            });
        }

        break;
    }

  };

  /* PUBLIC METHODS
   * ========================= */
  Backstretch.prototype = {

    resize: function () {
      try {

        // Check for a better suited image after the resize
        var $resTest = this.options.alwaysTestWindowResolution ? $(window) : this.$root;
        var newContainerWidth = $resTest.width();
        var newContainerHeight = $resTest.height();
        var changeRatioW = newContainerWidth / (this._lastResizeContainerWidth || 0);
        var changeRatioH = newContainerHeight / (this._lastResizeContainerHeight || 0);
        var resolutionChangeRatioThreshold = this.options.resolutionChangeRatioThreshold || 0.0;

        // check for big changes in container size
        if ((newContainerWidth !== this._lastResizeContainerWidth ||
          newContainerHeight !== this._lastResizeContainerHeight) &&
          ((Math.abs(changeRatioW - 1) >= resolutionChangeRatioThreshold || isNaN(changeRatioW)) ||
            (Math.abs(changeRatioH - 1) >= resolutionChangeRatioThreshold || isNaN(changeRatioH)))) {

          this._lastResizeContainerWidth = newContainerWidth;
          this._lastResizeContainerHeight = newContainerHeight;

          // Big change: rebuild the entire images array
          this.images = optimalSizeImages($resTest, this.originalImages);

          // Preload them (they will be automatically inserted on the next cycle)
          if (this.options.preload) {
            preload(this.images, (this.index + 1) % this.images.length, this.options.preload);
          }

          // In case there is no cycle and the new source is different than the current
          if (this.images.length === 1 &&
            this._currentImage.url !== this.images[0].url) {

            // Wait a little an update the image being showed
            var that = this;
            clearTimeout(that._selectAnotherResolutionTimeout);
            that._selectAnotherResolutionTimeout = setTimeout(function () {
              that.show(0);
            }, this.options.resolutionRefreshRate);
          }
        }

        var bgCSS = {left: 0, top: 0, right: 'auto', bottom: 'auto'}

          , boxWidth = this.isBody ? this.$root.width() : this.$root.innerWidth()
          , boxHeight = this.isBody
          ? (window.innerHeight ? window.innerHeight : this.$root.height())
          : this.$root.innerHeight()

          , naturalWidth = this.$itemWrapper.data('width')
          , naturalHeight = this.$itemWrapper.data('height')

          , ratio = (naturalWidth / naturalHeight) || 1

          , alignX = this._currentImage.alignX === undefined ? this.options.alignX : this._currentImage.alignX
          , alignY = this._currentImage.alignY === undefined ? this.options.alignY : this._currentImage.alignY
          , scale = validScale(this._currentImage.scale || this.options.scale);

        var width, height;

        if (scale === 'fit' || scale === 'fit-smaller') {
          width = naturalWidth;
          height = naturalHeight;

          if (width > boxWidth ||
            height > boxHeight ||
            scale === 'fit-smaller') {
            var boxRatio = boxWidth / boxHeight;
            if (boxRatio > ratio) {
              width = Math.floor(boxHeight * ratio);
              height = boxHeight;
            }
            else if (boxRatio < ratio) {
              width = boxWidth;
              height = Math.floor(boxWidth / ratio);
            }
            else {
              width = boxWidth;
              height = boxHeight;
            }
          }
        }
        else if (scale === 'fill') {
          width = boxWidth;
          height = boxHeight;
        }
        else { // 'cover'
          width = Math.max(boxHeight * ratio, boxWidth);
          height = Math.max(width / ratio, boxHeight);
        }

        // Make adjustments based on image ratio
        bgCSS.top = -(height - boxHeight) * alignY;
        bgCSS.left = -(width - boxWidth) * alignX;
        bgCSS.width = width;
        bgCSS.height = height;

        if (!this.options.bypassCss) {

          this.$wrap
              .css({width: boxWidth, height: boxHeight})
              .find('>.backstretch-item').not('.deleteable')
              .each(function () {
                var $wrapper = $(this);
                $wrapper.find('img,video,iframe')
                        .css(bgCSS);
              });
        }

        var evt = $.Event('backstretch.resize', {
          relatedTarget: this.$container[0]
        });
        this.$container.trigger(evt, this);

      }
      catch (err) {
        // IE7 seems to trigger resize before the image is loaded.
        // This try/catch block is a hack to let it fail gracefully.
      }

      return this;
    }

    // Show the slide at a certain position
    , show: function (newIndex, overrideOptions) {

      // Validate index
      if (Math.abs(newIndex) > this.images.length - 1) {
        return;
      }

      // Vars
      var that = this
        , $oldItemWrapper = that.$wrap.find('>.backstretch-item').addClass('deleteable')
        , oldVideoWrapper = that.videoWrapper
        , evtOptions = {relatedTarget: that.$container[0]};

      // Trigger the "before" event
      that.$container.trigger($.Event('backstretch.before', evtOptions), [that, newIndex]);

      // Set the new frame index
      this.index = newIndex;
      var selectedImage = that.images[newIndex];

      // Pause the slideshow
      clearTimeout(that._cycleTimeout);

      // New image

      delete that.videoWrapper; // Current item may not be a video

      var isVideo = isVideoSource(selectedImage);
      if (isVideo) {
        that.videoWrapper = new VideoWrapper(selectedImage);
        that.$item = that.videoWrapper.$video.css('pointer-events', 'none');
      }
      else {
        that.$item = $('<img />');
      }

      that.$itemWrapper = $('<div class="backstretch-item">')
        .append(that.$item);

      if (this.options.bypassCss) {
        that.$itemWrapper.css({
          'display': 'none'
        });
      }
      else {
        that.$itemWrapper.css(styles.itemWrapper);
        that.$item.css(styles.item);
      }

      that.$item.bind(isVideo ? 'canplay' : 'load', function (e) {
        var $this = $(this)
          , $wrapper = $this.parent()
          , options = $wrapper.data('options');

        if (overrideOptions) {
          options = $.extend({}, options, overrideOptions);
        }

        var imgWidth = this.naturalWidth || this.videoWidth || this.width
          , imgHeight = this.naturalHeight || this.videoHeight || this.height;

        // Save the natural dimensions
        $wrapper
          .data('width', imgWidth)
          .data('height', imgHeight);

        var getOption = function (opt) {
          return options[opt] !== undefined ?
            options[opt] :
            that.options[opt];
        };

        var transition = getOption('transition');
        var transitionEasing = getOption('transitionEasing');
        var transitionDuration = getOption('transitionDuration');

        // Show the image, then delete the old one
        var bringInNextImage = function () {

          if (oldVideoWrapper) {
            oldVideoWrapper.stop();
            oldVideoWrapper.destroy();
          }

          $oldItemWrapper.remove();

          // Resume the slideshow
          if (!that.paused && that.images.length > 1) {
            that.cycle();
          }

          // Now we can clear the background on the element, to spare memory
          if (!that.options.bypassCss && !that.isBody) {
            that.$container.css('background-image', 'none');
          }

          // Trigger the "after" and "show" events
          // "show" is being deprecated
          $(['after', 'show']).each(function () {
            that.$container.trigger($.Event('backstretch.' + this, evtOptions), [that, newIndex]);
          });

          if (isVideo) {
            that.videoWrapper.play();
          }
        };

        if ((that.firstShow && !that.options.animateFirst) || !transitionDuration || !transition) {
          // Avoid transition on first show or if there's no transitionDuration value
          $wrapper.show();
          bringInNextImage();
        }
        else {

          performTransition({
            'new': $wrapper,
            old: $oldItemWrapper,
            transition: transition,
            duration: transitionDuration,
            easing: transitionEasing,
            complete: bringInNextImage
          });

        }

        that.firstShow = false;

        // Resize
        that.resize();
      });

      that.$itemWrapper.appendTo(that.$wrap);

      that.$item.attr('alt', selectedImage.alt || '');
      that.$itemWrapper.data('options', selectedImage);

      if (!isVideo) {
        that.$item.attr('src', selectedImage.url);
      }

      that._currentImage = selectedImage;

      return that;
    }

    , current: function () {
      return this.index;
    }

    , next: function () {
      var args = Array.prototype.slice.call(arguments, 0);
      args.unshift(this.index < this.images.length - 1 ? this.index + 1 : 0);
      return this.show.apply(this, args);
    }

    , prev: function () {
      var args = Array.prototype.slice.call(arguments, 0);
      args.unshift(this.index === 0 ? this.images.length - 1 : this.index - 1);
      return this.show.apply(this, args);
    }

    , pause: function () {
      // Pause the slideshow
      this.paused = true;

      if (this.videoWrapper) {
        this.videoWrapper.pause();
      }

      return this;
    }

    , resume: function () {
      // Resume the slideshow
      this.paused = false;

      if (this.videoWrapper) {
        this.videoWrapper.play();
      }

      this.cycle();
      return this;
    }

    , cycle: function () {
      // Start/resume the slideshow
      if (this.images.length > 1) {
        // Clear the timeout, just in case
        clearTimeout(this._cycleTimeout);

        var duration = (this._currentImage && this._currentImage.duration) || this.options.duration;
        var isVideo = isVideoSource(this._currentImage);

        var callNext = function () {
          this.$item.off('.cycle');

          // Check for paused slideshow
          if (!this.paused) {
            this.next();
          }
        };

        // Special video handling
        if (isVideo) {

          // Leave video at last frame
          if (!this._currentImage.loop) {
            var lastFrameTimeout = 0;

            this.$item
                .on('playing.cycle', function () {
                  var player = $(this).data('player');

                  clearTimeout(lastFrameTimeout);
                  lastFrameTimeout = setTimeout(function () {
                    player.pause();
                    player.$video.trigger('ended');
                  }, (player.getDuration() - player.getCurrentTime()) * 1000);
                })
                .on('ended.cycle', function () {
                  clearTimeout(lastFrameTimeout);
                });
          }

          // On error go to next
          this.$item.on('error.cycle initerror.cycle', $.proxy(callNext, this));
        }

        if (isVideo && !this._currentImage.duration) {
          // It's a video - playing until end
          this.$item.on('ended.cycle', $.proxy(callNext, this));

        }
        else {
          // Cycling according to specified duration
          this._cycleTimeout = setTimeout($.proxy(callNext, this), duration);
        }

      }
      return this;
    }

    , destroy: function (preserveBackground) {
      // Stop the resize events
      $(window).off('resize.backstretch orientationchange.backstretch');

      // Stop any videos
      if (this.videoWrapper) {
        this.videoWrapper.destroy();
      }

      // Clear the timeout
      clearTimeout(this._cycleTimeout);

      // Remove Backstretch
      if (!preserveBackground) {
        this.$wrap.remove();
      }
      this.$container.removeData('backstretch');
    }
  };

  /**
   * Video Abstraction Layer
   *
   * Static methods:
   * > VideoWrapper.loadYoutubeAPI() -> Call in order to load the Youtube API.
   *                                   An 'youtube_api_load' event will be triggered on $(window) when the API is loaded.
   *
   * Generic:
   * > player.type -> type of the video
   * > player.video / player.$video -> contains the element holding the video
   * > player.play() -> plays the video
   * > player.pause() -> pauses the video
   * > player.setCurrentTime(position) -> seeks to a position by seconds
   *
   * Youtube:
   * > player.ytId will contain the youtube ID if the source is a youtube url
   * > player.ytReady is a flag telling whether the youtube source is ready for playback
   * */

  var VideoWrapper = function () { this.init.apply(this, arguments); };

  /**
   * @param {Object} options
   * @param {String|Array<String>|Array<{{src: String, type: String?}}>} options.url
   * @param {Boolean} options.loop=false
   * @param {Boolean?} options.mute=true
   * @param {String?} options.poster
   * loop, mute, poster
   */
  VideoWrapper.prototype.init = function (options) {

    var that = this;

    var $video;

    var setVideoElement = function () {
      that.$video = $video;
      that.video = $video[0];
    };

    // Determine video type

    var videoType = 'video';

    if (!(options.url instanceof Array) &&
      YOUTUBE_REGEXP.test(options.url)) {
      videoType = 'youtube';
    }

    that.type = videoType;

    if (videoType === 'youtube') {

      // Try to load the API in the meantime
      VideoWrapper.loadYoutubeAPI();

      that.ytId = options.url.match(YOUTUBE_REGEXP)[2];
      var src = 'https://www.youtube.com/embed/' + that.ytId +
        '?rel=0&autoplay=0&showinfo=0&controls=0&modestbranding=1' +
        '&cc_load_policy=0&disablekb=1&iv_load_policy=3&loop=0' +
        '&enablejsapi=1&origin=' + encodeURIComponent(window.location.origin);

      that.__ytStartMuted = !!options.mute || options.mute === undefined;

      $video = $('<iframe />')
        .attr({'src_to_load': src})
        .css({'border': 0, 'margin': 0, 'padding': 0})
        .data('player', that);

      if (options.loop) {
        $video.on('ended.loop', function () {
          if (!that.__manuallyStopped) {
            that.play();
          }
        });
      }

      that.ytReady = false;

      setVideoElement();

      if (window['YT'] && window['YT'].loaded) {
        that._initYoutube();
        $video.trigger('initsuccess');
      }
      else {
        $(window).one('youtube_api_load', function () {
          that._initYoutube();
          $video.trigger('initsuccess');
        });
      }

    }
    else {
      // Traditional <video> tag with multiple sources

      $video = $('<video>')
        .prop('autoplay', false)
        .prop('controls', false)
        .prop('loop', !!options.loop)
        .prop('muted', !!options.mute || options.mute === undefined)

        // Let the first frames be available before playback, as we do transitions
        .prop('preload', 'auto')
        .prop('poster', options.poster || '');

      var sources = (options.url instanceof Array) ? options.url : [options.url];

      for (var i = 0; i < sources.length; i++) {
        var sourceItem = sources[i];
        if (typeof (sourceItem) === 'string') {
          sourceItem = {src: sourceItem};
        }
        $('<source>')
          .attr('src', sourceItem.src)
          // Make sure to not specify type if unknown -
          //   so the browser will try to autodetect.
          .attr('type', sourceItem.type || null)
          .appendTo($video);
      }

      if (!$video[0].canPlayType || !sources.length) {
        $video.trigger('initerror');
      }
      else {
        $video.trigger('initsuccess');
      }

      setVideoElement();
    }

  };

  VideoWrapper.prototype._initYoutube = function () {
    var that = this;

    var YT = window['YT'];

    that.$video
        .attr('src', that.$video.attr('src_to_load'))
        .removeAttr('src_to_load');

    // It won't init if it's not in the DOM, so we emulate that
    var hasParent = !!that.$video[0].parentNode;
    if (!hasParent) {
      var $tmpParent = $('<div>').css('display', 'none !important').appendTo(document.body);
      that.$video.appendTo($tmpParent);
    }

    var player = new YT.Player(that.video, {
      events: {
        'onReady': function () {

          if (that.__ytStartMuted) {
            player.mute();
          }

          if (!hasParent) {
            // Restore parent to old state - without interrupting any changes
            if (that.$video[0].parentNode === $tmpParent[0]) {
              that.$video.detach();
            }
            $tmpParent.remove();
          }

          that.ytReady = true;
          that._updateYoutubeSize();
          that.$video.trigger('canplay');
        },
        'onStateChange': function (event) {
          switch (event.data) {
            case YT.PlayerState.PLAYING:
              that.$video.trigger('playing');
              break;
            case YT.PlayerState.ENDED:
              that.$video.trigger('ended');
              break;
            case YT.PlayerState.PAUSED:
              that.$video.trigger('pause');
              break;
            case YT.PlayerState.BUFFERING:
              that.$video.trigger('waiting');
              break;
            case YT.PlayerState.CUED:
              that.$video.trigger('canplay');
              break;
          }
        },
        'onPlaybackQualityChange': function () {
          that._updateYoutubeSize();
          that.$video.trigger('resize');
        },
        'onError': function (err) {
          that.hasError = true;
          that.$video.trigger({'type': 'error', 'error': err});
        }
      }
    });

    that.ytPlayer = player;

    return that;
  };

  VideoWrapper.prototype._updateYoutubeSize = function () {
    var that = this;

    switch (that.ytPlayer.getPlaybackQuality() || 'medium') {
      case 'small':
        that.video.videoWidth = 426;
        that.video.videoHeight = 240;
        break;
      case 'medium':
        that.video.videoWidth = 640;
        that.video.videoHeight = 360;
        break;
      default:
      case 'large':
        that.video.videoWidth = 854;
        that.video.videoHeight = 480;
        break;
      case 'hd720':
        that.video.videoWidth = 1280;
        that.video.videoHeight = 720;
        break;
      case 'hd1080':
        that.video.videoWidth = 1920;
        that.video.videoHeight = 1080;
        break;
      case 'highres':
        that.video.videoWidth = 2560;
        that.video.videoHeight = 1440;
        break;
    }

    return that;
  };

  VideoWrapper.prototype.play = function () {
    var that = this;

    that.__manuallyStopped = false;

    if (that.type === 'youtube') {
      if (that.ytReady) {
        that.$video.trigger('play');
        that.ytPlayer.playVideo();
      }
    }
    else {
      that.video.play();
    }

    return that;
  };

  VideoWrapper.prototype.pause = function () {
    var that = this;

    that.__manuallyStopped = false;

    if (that.type === 'youtube') {
      if (that.ytReady) {
        that.ytPlayer.pauseVideo();
      }
    }
    else {
      that.video.pause();
    }

    return that;
  };

  VideoWrapper.prototype.stop = function () {
    var that = this;

    that.__manuallyStopped = true;

    if (that.type === 'youtube') {
      if (that.ytReady) {
        that.ytPlayer.pauseVideo();
        that.ytPlayer.seekTo(0);
      }
    }
    else {
      that.video.pause();
      that.video.currentTime = 0;
    }

    return that;
  };

  VideoWrapper.prototype.destroy = function () {
    var that = this;

    if (that.ytPlayer) {
      that.ytPlayer.destroy();
    }

    that.$video.remove();

    return that;
  };

  VideoWrapper.prototype.getCurrentTime = function (seconds) {
    var that = this;

    if (that.type === 'youtube') {
      if (that.ytReady) {
        return that.ytPlayer.getCurrentTime();
      }
    }
    else {
      return that.video.currentTime;
    }

    return 0;
  };

  VideoWrapper.prototype.setCurrentTime = function (seconds) {
    var that = this;

    if (that.type === 'youtube') {
      if (that.ytReady) {
        that.ytPlayer.seekTo(seconds, true);
      }
    }
    else {
      that.video.currentTime = seconds;
    }

    return that;
  };

  VideoWrapper.prototype.getDuration = function () {
    var that = this;

    if (that.type === 'youtube') {
      if (that.ytReady) {
        return that.ytPlayer.getDuration();
      }
    }
    else {
      return that.video.duration;
    }

    return 0;
  };

  /**
   * This will load the youtube API (if not loaded yet)
   * Use $(window).one('youtube_api_load', ...) to listen for API loaded event
   */
  VideoWrapper.loadYoutubeAPI = function () {
    if (window['YT'] && window['__yt_load_event_interval__']) {
      return;
    }
    
    if (!window['YT'] && !$('script[src*=www\\.youtube\\.com\\/iframe_api]').length) {
      $('<script type="text/javascript" src="https://www.youtube.com/iframe_api">').appendTo('body');
    }
    
    window['__yt_load_event_interval__'] = setInterval(function () {
      if (window['YT'] && window['YT'].loaded) {
        $(window).trigger('youtube_api_load');
        clearTimeout(window['__yt_load_event_interval__']);
        delete window['__yt_load_event_interval__'];
      }
    }, 50);
  };

  var getDeviceOrientation = function () {

    if ('matchMedia' in window) {
      if (window.matchMedia("(orientation: portrait)").matches) {
        return 'portrait';
      }
      else if (window.matchMedia("(orientation: landscape)").matches) {
        return 'landscape';
      }
    }

    if (screen.height > screen.width) {
      return 'portrait';
    }

    // Even square devices have orientation,
    //   but a desktop browser may be too old for `matchMedia`.
    // Defaulting to `landscape` for the VERY rare case of a square desktop screen is good enough.
    return 'landscape';
  };

  var getWindowOrientation = function () {
    if (window.innerHeight > window.innerWidth) {
      return 'portrait';
    }
    if (window.innerWidth > window.innerHeight) {
      return 'landscape';
    }

    return 'square';
  };

  /* SUPPORTS FIXED POSITION?
   *
   * Based on code from jQuery Mobile 1.1.0
   * http://jquerymobile.com/
   *
   * In a nutshell, we need to figure out if fixed positioning is supported.
   * Unfortunately, this is very difficult to do on iOS, and usually involves
   * injecting content, scrolling the page, etc.. It's ugly.
   * jQuery Mobile uses this workaround. It's not ideal, but works.
   *
   * Modified to detect IE6
   * ========================= */

  var supportsFixedPosition = (function () {
    var ua = navigator.userAgent
      , platform = navigator.platform
      // Rendering engine is Webkit, and capture major version
      , wkmatch = ua.match(/AppleWebKit\/([0-9]+)/)
      , wkversion = !!wkmatch && wkmatch[1]
      , ffmatch = ua.match(/Fennec\/([0-9]+)/)
      , ffversion = !!ffmatch && ffmatch[1]
      , operammobilematch = ua.match(/Opera Mobi\/([0-9]+)/)
      , omversion = !!operammobilematch && operammobilematch[1]
      , iematch = ua.match(/MSIE ([0-9]+)/)
      , ieversion = !!iematch && iematch[1];

    return !(
      // iOS 4.3 and older : Platform is iPhone/Pad/Touch and Webkit version is less than 534 (ios5)
      ((platform.indexOf("iPhone") > -1 || platform.indexOf("iPad") > -1 || platform.indexOf(
        "iPod") > -1) && wkversion && wkversion < 534) ||

      // Opera Mini
      (window.operamini && ({}).toString.call(window.operamini) === "[object OperaMini]") ||
      (operammobilematch && omversion < 7458) ||

      //Android lte 2.1: Platform is Android and Webkit version is less than 533 (Android 2.2)
      (ua.indexOf("Android") > -1 && wkversion && wkversion < 533) ||

      // Firefox Mobile before 6.0 -
      (ffversion && ffversion < 6) ||

      // WebOS less than 3
      ("palmGetResource" in window && wkversion && wkversion < 534) ||

      // MeeGo
      (ua.indexOf("MeeGo") > -1 && ua.indexOf("NokiaBrowser/8.5.0") > -1) ||

      // IE6
      (ieversion && ieversion <= 6)
    );
  }());

}(jQuery, window));
