/*
 * Cloud 9 Carousel 2.2.0
 *
 * Pseudo-3D carousel plugin for jQuery/Zepto focused on performance.
 *
 * Based on the original CloudCarousel by R. Cecco.
 *
 * See the demo and download the latest version:
 *   http://specious.github.io/cloud9carousel/
 *
 * Copyright (c) 2017 by Ildar Sagdejev ( http://specious.github.io )
 * Copyright (c) 2011 by R. Cecco ( http://www.professorcloud.com )
 *
 * MIT License
 *
 * Please retain this copyright header in all versions of the software
 *
 * Requires:
 *  - jQuery >= 1.3.0 or Zepto >= 1.1.1
 *
 * Optional (jQuery only):
 *  - Reflection support via reflection.js plugin by Christophe Beyls
 *     http://www.digitalia.be/software/reflectionjs-for-jquery
 *  - Mousewheel support via mousewheel plugin
 *     http://plugins.jquery.com/mousewheel/
 */

;(function($) {
  //
  // Detect CSS transform support
  //
  var transform = (function() {
    var vendors = ['webkit', 'moz', 'ms'];
    var style   = document.createElement( "div" ).style;
    var trans   = 'transform' in style ? 'transform' : undefined;

    for( var i = 0, count = vendors.length; i < count; i++ ) {
      var prop = vendors[i] + 'Transform';
      if( prop in style ) {
        trans = prop;
        break;
      }
    }

    return trans;
  })();

  var Item = function( element, options ) {
    element.item = this;
    this.element = element;

    if( element.tagName === 'IMG' ) {
      this.fullWidth = element.width;
      this.fullHeight = element.height;
    } else {
      element.style.display = "inline-block";
      this.fullWidth = element.offsetWidth;
      this.fullHeight = element.offsetHeight;
    }

    element.style.position = 'absolute';

    if( options.mirror && this.element.tagName === 'IMG' ) {
      // Wrap image in a div together with its generated reflection
      this.reflection = $(element).reflect( options.mirror ).next()[0];

      var $reflection = $(this.reflection);
      this.reflection.fullHeight = $reflection.height();
      $reflection.css( 'margin-top', options.mirror.gap + 'px' );
      $reflection.css( 'width', '100%' );
      element.style.width = "100%";

      // The item element now contains the image and reflection
      this.element = this.element.parentNode;
      this.element.item  = this;
      this.element.alt   = element.alt;
      this.element.title = element.title;
    }

    if( transform && options.transforms )
      this.element.style[transform + "Origin"] = "0 0";

    this.moveTo = function( x, y, scale ) {
      this.width = this.fullWidth * scale;
      this.height = this.fullHeight * scale;
      this.x = x;
      this.y = y;
      this.scale = scale;

      var style = this.element.style;
      style.zIndex = "" + (scale * 100) | 0;

      if( transform && options.transforms ) {
        style[transform] = "translate(" + x + "px, " + y + "px) scale(" + scale + ")";
      } else {
        // Manually resize the gap between the image and its reflection
        if( options.mirror && this.element.tagName === 'IMG' )
          this.reflection.style.marginTop = (options.mirror.gap * scale) + "px";

        style.width = this.width + "px";
        style.left = x + "px";
        style.top = y + "px";
      }
    }
  }

  var time = !window.performance || !window.performance.now ?
    function() { return +new Date() } :
    function() { return performance.now() };

  //
  // Detect requestAnimationFrame() support
  //
  // Support legacy browsers:
  //   http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
  //
  var cancelFrame = window.cancelAnimationFrame || window.cancelRequestAnimationFrame;
  var requestFrame = window.requestAnimationFrame;

  (function() {
    var vendors = ['webkit', 'moz', 'ms'];

    for( var i = 0, count = vendors.length; i < count && !cancelFrame; i++ ) {
      cancelFrame = window[vendors[i]+'CancelAnimationFrame'] || window[vendors[i]+'CancelRequestAnimationFrame'];
      requestFrame = requestFrame && window[vendors[i]+'RequestAnimationFrame'];
    }
  }());

  var Carousel = function( element, options ) {
    var self = this;
    var $container = $(element);
    this.items = [];
    this.xOrigin = (options.xOrigin === null) ? $container.width()  * 0.5 : options.xOrigin;
    this.yOrigin = (options.yOrigin === null) ? $container.height() * 0.1 : options.yOrigin;
    this.xRadius = (options.xRadius === null) ? $container.width()  / 2.3 : options.xRadius;
    this.yRadius = (options.yRadius === null) ? $container.height() / 6   : options.yRadius;
    this.farScale = options.farScale;
    this.rotation = this.destRotation = Math.PI/2; // start with the first item positioned in front
    this.speed = options.speed;
    this.smooth = options.smooth;
    this.fps = options.fps;
    this.timer = 0;
    this.autoPlayAmount = options.autoPlay;
    this.autoPlayDelay = options.autoPlayDelay;
    this.autoPlayTimer = 0;
    this.frontItemClass = options.frontItemClass;
    this.onLoaded = options.onLoaded;
    this.onRendered = options.onRendered;
    this.onAnimationFinished = options.onAnimationFinished;

    this.itemOptions = {
      transforms: options.transforms
    }

    if( options.mirror ) {
      this.itemOptions.mirror = $.extend( { gap: 2 }, options.mirror );
    }

    $container.css( { position: 'relative', overflow: 'hidden' } );

    // Rotation:
    //  *      0 : right
    //  *   Pi/2 : front
    //  *   Pi   : left
    //  * 3 Pi/2 : back
    this.renderItem = function( itemIndex, rotation ) {
      var item = this.items[itemIndex];
      var sin = Math.sin(rotation);
      var farScale = this.farScale;
      var scale = farScale + ((1-farScale) * (sin+1) * 0.5);

      item.moveTo(
        this.xOrigin + (scale * ((Math.cos(rotation) * this.xRadius) - (item.fullWidth * 0.5))),
        this.yOrigin + (scale * sin * this.yRadius),
        scale
      );

      return item;
    }

    this.render = function() {
      var count = this.items.length;
      var spacing = 2 * Math.PI / count;
      var radians = this.rotation;
      var nearest = this.nearestIndex();

      for( var i = 0; i < count; i++ ) {
        var item = this.renderItem( i, radians );

        if( i === nearest )
          $(item.element).addClass( this.frontItemClass );
        else
          $(item.element).removeClass( this.frontItemClass );

        radians += spacing;
      }

      if( typeof this.onRendered === 'function' )
        this.onRendered( this );
    }

    this.playFrame = function() {
      var rem = self.destRotation - self.rotation;
      var now = time();
      var dt = (now - self.lastTime) * 0.002;
      self.lastTime = now;

      if( Math.abs(rem) < 0.003 ) {
        self.rotation = self.destRotation;
        self.pause();

        if( typeof self.onAnimationFinished === 'function' )
          self.onAnimationFinished();
      } else {
        // Asymptotically approach the destination
        self.rotation = self.destRotation - rem / (1 + (self.speed * dt));
        self.scheduleNextFrame();
      }

      self.render();
    }

    this.scheduleNextFrame = function() {
      this.lastTime = time();

      this.timer = this.smooth && cancelFrame ?
        requestFrame( self.playFrame ) :
        setTimeout( self.playFrame, 1000 / this.fps );
    }

    this.itemsRotated = function() {
      return this.items.length * ((Math.PI/2) - this.rotation) / (2*Math.PI);
    }

    this.floatIndex = function() {
      var count = this.items.length;
      var floatIndex = this.itemsRotated() % count;

      // Make sure float-index is positive
      return (floatIndex < 0) ? floatIndex + count : floatIndex;
    }

    this.nearestIndex = function() {
      return Math.round( this.floatIndex() ) % this.items.length;
    }

    this.nearestItem = function() {
      return this.items[this.nearestIndex()];
    }

    this.play = function() {
      if( this.timer === 0 )
        this.scheduleNextFrame();
    }

    this.pause = function() {
      this.smooth && cancelFrame ? cancelFrame( this.timer ) : clearTimeout( this.timer );
      this.timer = 0;
    }

    //
    // Spin the carousel by (+-) count items
    //
    this.go = function( count ) {
      this.destRotation += (2 * Math.PI / this.items.length) * count;
      this.play();
    }

    this.goTo = function( index ) {
      var count = this.items.length;

      // Find the shortest way to rotate item to front
      var diff = index - (this.floatIndex() % count);

      if( 2 * Math.abs(diff) > count )
        diff -= (diff > 0) ? count : -count;

      // Halt any rotation already in progress
      this.destRotation = this.rotation;

      // Spin the opposite way to bring item to front
      this.go( -diff );

      // Return rotational distance (in items) to the target
      return diff;
    }

    this.deactivate = function() {
      this.pause();
      clearInterval( this.autoPlayTimer );
      if( options.buttonLeft ) options.buttonLeft.unbind( 'click' );
      if( options.buttonRight ) options.buttonRight.unbind( 'click' );
      $container.unbind( '.cloud9' );
    }

    this.autoPlay = function() {
      this.autoPlayTimer = setInterval(
        function() { self.go( self.autoPlayAmount ) },
        this.autoPlayDelay
      );
    }

    this.enableAutoPlay = function() {
      // Stop auto-play on mouse over
      $container.bind( 'mouseover.cloud9', function() {
        clearInterval( self.autoPlayTimer );
      } );

      // Resume auto-play when mouse leaves the container
      $container.bind( 'mouseout.cloud9', function() {
        self.autoPlay();
      } );

      this.autoPlay();
    }

    this.bindControls = function() {
      if( options.buttonLeft ) {
        options.buttonLeft.bind( 'click', function() {
          self.go( -1 );
          return false;
        } );
      }

      if( options.buttonRight ) {
        options.buttonRight.bind( 'click', function() {
          self.go( 1 );
          return false;
        } );
      }

      if( options.mouseWheel ) {
        $container.bind( 'mousewheel.cloud9', function( event, delta ) {
          self.go( (delta > 0) ? 1 : -1 );
          return false;
        } );
      }

      if( options.bringToFront ) {
        $container.bind( 'click.cloud9', function( event ) {
          var hits = $(event.target).closest( '.' + options.itemClass );

          if( hits.length !== 0 ) {
            var diff = self.goTo( self.items.indexOf( hits[0].item ) );

            // Suppress default browser action if the item isn't roughly in front
            if( Math.abs(diff) > 0.5 )
              event.preventDefault();
          }
        } );
      }
    }

    var items = $container.find( '.' + options.itemClass );

    this.finishInit = function() {
      //
      // Wait until all images have completely loaded
      //
      for( var i = 0; i < items.length; i++ ) {
        var item = items[i];
        if( (item.tagName === 'IMG') &&
            ((item.width === undefined) || ((item.complete !== undefined) && !item.complete)) )
          return;
      }

      clearInterval( this.initTimer );

      // Init items
      for( i = 0; i < items.length; i++ )
        this.items.push( new Item( items[i], this.itemOptions ) );

      // Disable click-dragging of items
      $container.bind( 'mousedown onselectstart', function() { return false } );

      if( this.autoPlayAmount !== 0 ) this.enableAutoPlay();
      this.bindControls();
      this.render();

      if( typeof this.onLoaded === 'function' )
        this.onLoaded( this );
    };

    this.initTimer = setInterval( function() { self.finishInit() }, 50 );
  }

  //
  // The jQuery plugin
  //
  $.fn.Cloud9Carousel = function( options ) {
    return this.each( function() {
      /* For full list of options see the README */
      options = $.extend( {
        xOrigin: null,        // null: calculated automatically
        yOrigin: null,
        xRadius: null,
        yRadius: null,
        farScale: 0.5,        // scale of the farthest item
        transforms: true,     // enable CSS transforms
        smooth: true,         // enable smooth animation via requestAnimationFrame()
        fps: 30,              // fixed frames per second (if smooth animation is off)
        speed: 4,             // positive number
        autoPlay: 0,          // [ 0: off | number of items (integer recommended, positive is clockwise) ]
        autoPlayDelay: 4000,
        bringToFront: false,
        itemClass: 'cloud9-item',
        frontItemClass: null,
        handle: 'carousel'
      }, options );

      $(this).data( options.handle, new Carousel( this, options ) );
    } );
  }
})( window.jQuery || window.Zepto );