// drag
( function( window, factory ) {
  // universal module definition
  if ( typeof module == 'object' && module.exports ) {
    // CommonJS
    module.exports = factory(
        window,
        require('./core'),
        require('unidragger'),
        require('fizzy-ui-utils'),
    );
  } else {
    // browser global
    window.Flickity = factory(
        window,
        window.Flickity,
        window.Unidragger,
        window.fizzyUIUtils,
    );
  }

}( typeof window != 'undefined' ? window : this,
    function factory( window, Flickity, Unidragger, utils ) {

// ----- defaults ----- //

Object.assign( Flickity.defaults, {
  draggable: '>1',
  dragThreshold: 3,
} );

// -------------------------- drag prototype -------------------------- //

let proto = Flickity.prototype;
Object.assign( proto, Unidragger.prototype ); // inherit Unidragger
proto.touchActionValue = '';

// --------------------------  -------------------------- //

Flickity.create.drag = function() {
  this.on( 'activate', this.onActivateDrag );
  this.on( 'uiChange', this._uiChangeDrag );
  this.on( 'deactivate', this.onDeactivateDrag );
  this.on( 'cellChange', this.updateDraggable );
  this.on( 'pointerDown', this.handlePointerDown );
  this.on( 'pointerUp', this.handlePointerUp );
  this.on( 'pointerDown', this.handlePointerDone );
  this.on( 'dragStart', this.handleDragStart );
  this.on( 'dragMove', this.handleDragMove );
  this.on( 'dragEnd', this.handleDragEnd );
  this.on( 'staticClick', this.handleStaticClick );
  // TODO updateDraggable on resize? if groupCells & slides change
};

proto.onActivateDrag = function() {
  this.handles = [ this.viewport ];
  this.bindHandles();
  this.updateDraggable();
};

proto.onDeactivateDrag = function() {
  this.unbindHandles();
  this.element.classList.remove('is-draggable');
};

proto.updateDraggable = function() {
  // disable dragging if less than 2 slides. #278
  if ( this.options.draggable === '>1' ) {
    this.isDraggable = this.slides.length > 1;
  } else {
    this.isDraggable = this.options.draggable;
  }
  this.element.classList.toggle( 'is-draggable', this.isDraggable );
};

proto._uiChangeDrag = function() {
  delete this.isFreeScrolling;
};

// -------------------------- pointer events -------------------------- //

proto.handlePointerDown = function( event ) {
  if ( !this.isDraggable ) {
    // proceed for staticClick
    this.bindActivePointerEvents( event );
    return;
  }

  let isTouchStart = event.type === 'touchstart';
  let isTouchPointer = event.pointerType === 'touch';
  let isFocusNode = event.target.matches('input, textarea, select');
  if ( !isTouchStart && !isTouchPointer && !isFocusNode ) event.preventDefault();
  if ( !isFocusNode ) this.focus();
  // blur
  if ( document.activeElement !== this.element ) document.activeElement.blur();
  // stop if it was moving
  this.dragX = this.x;
  this.viewport.classList.add('is-pointer-down');
  // track scrolling
  this.pointerDownScroll = getScrollPosition();
  window.addEventListener( 'scroll', this );
  this.bindActivePointerEvents( event );
};

// ----- move ----- //

proto.hasDragStarted = function( moveVector ) {
  return Math.abs( moveVector.x ) > this.options.dragThreshold;
};

// ----- up ----- //

proto.handlePointerUp = function() {
  delete this.isTouchScrolling;
  this.viewport.classList.remove('is-pointer-down');
};

proto.handlePointerDone = function() {
  window.removeEventListener( 'scroll', this );
  delete this.pointerDownScroll;
};

// -------------------------- dragging -------------------------- //

proto.handleDragStart = function() {
  if ( !this.isDraggable ) return;

  this.dragStartPosition = this.x;
  this.startAnimation();
  window.removeEventListener( 'scroll', this );
};

proto.handleDragMove = function( event, pointer, moveVector ) {
  if ( !this.isDraggable ) return;

  event.preventDefault();

  this.previousDragX = this.dragX;
  // reverse if right-to-left
  let direction = this.options.rightToLeft ? -1 : 1;
  // wrap around move. #589
  if ( this.isWrapping ) moveVector.x %= this.slideableWidth;
  let dragX = this.dragStartPosition + moveVector.x * direction;

  if ( !this.isWrapping ) {
    // slow drag
    let originBound = Math.max( -this.slides[0].target, this.dragStartPosition );
    dragX = dragX > originBound ? ( dragX + originBound ) * 0.5 : dragX;
    let endBound = Math.min( -this.getLastSlide().target, this.dragStartPosition );
    dragX = dragX < endBound ? ( dragX + endBound ) * 0.5 : dragX;
  }

  this.dragX = dragX;
  this.dragMoveTime = new Date();
};

proto.handleDragEnd = function() {
  if ( !this.isDraggable ) return;

  let { freeScroll } = this.options;
  if ( freeScroll ) this.isFreeScrolling = true;
  // set selectedIndex based on where flick will end up
  let index = this.dragEndRestingSelect();

  if ( freeScroll && !this.isWrapping ) {
    // if free-scroll & not wrap around
    // do not free-scroll if going outside of bounding slides
    // so bounding slides can attract slider, and keep it in bounds
    let restingX = this.getRestingPosition();
    this.isFreeScrolling = -restingX > this.slides[0].target &&
      -restingX < this.getLastSlide().target;
  } else if ( !freeScroll && index === this.selectedIndex ) {
    // boost selection if selected index has not changed
    index += this.dragEndBoostSelect();
  }
  delete this.previousDragX;
  // apply selection
  // HACK, set flag so dragging stays in correct direction
  this.isDragSelect = this.isWrapping;
  this.select( index );
  delete this.isDragSelect;
};

proto.dragEndRestingSelect = function() {
  let restingX = this.getRestingPosition();
  // how far away from selected slide
  let distance = Math.abs( this.getSlideDistance( -restingX, this.selectedIndex ) );
  // get closet resting going up and going down
  let positiveResting = this._getClosestResting( restingX, distance, 1 );
  let negativeResting = this._getClosestResting( restingX, distance, -1 );
  // use closer resting for wrap-around
  return positiveResting.distance < negativeResting.distance ?
    positiveResting.index : negativeResting.index;
};

/**
 * given resting X and distance to selected cell
 * get the distance and index of the closest cell
 * @param {Number} restingX - estimated post-flick resting position
 * @param {Number} distance - distance to selected cell
 * @param {Integer} increment - +1 or -1, going up or down
 * @returns {Object} - { distance: {Number}, index: {Integer} }
 */
proto._getClosestResting = function( restingX, distance, increment ) {
  let index = this.selectedIndex;
  let minDistance = Infinity;
  let condition = this.options.contain && !this.isWrapping ?
    // if containing, keep going if distance is equal to minDistance
    ( dist, minDist ) => dist <= minDist :
    ( dist, minDist ) => dist < minDist;

  while ( condition( distance, minDistance ) ) {
    // measure distance to next cell
    index += increment;
    minDistance = distance;
    distance = this.getSlideDistance( -restingX, index );
    if ( distance === null ) break;

    distance = Math.abs( distance );
  }
  return {
    distance: minDistance,
    // selected was previous index
    index: index - increment,
  };
};

/**
 * measure distance between x and a slide target
 * @param {Number} x - horizontal position
 * @param {Integer} index - slide index
 * @returns {Number} - slide distance
 */
proto.getSlideDistance = function( x, index ) {
  let len = this.slides.length;
  // wrap around if at least 2 slides
  let isWrapAround = this.options.wrapAround && len > 1;
  let slideIndex = isWrapAround ? utils.modulo( index, len ) : index;
  let slide = this.slides[ slideIndex ];
  if ( !slide ) return null;

  // add distance for wrap-around slides
  let wrap = isWrapAround ? this.slideableWidth * Math.floor( index/len ) : 0;
  return x - ( slide.target + wrap );
};

proto.dragEndBoostSelect = function() {
  // do not boost if no previousDragX or dragMoveTime
  if ( this.previousDragX === undefined || !this.dragMoveTime ||
    // or if drag was held for 100 ms
    new Date() - this.dragMoveTime > 100 ) {
    return 0;
  }

  let distance = this.getSlideDistance( -this.dragX, this.selectedIndex );
  let delta = this.previousDragX - this.dragX;
  if ( distance > 0 && delta > 0 ) {
    // boost to next if moving towards the right, and positive velocity
    return 1;
  } else if ( distance < 0 && delta < 0 ) {
    // boost to previous if moving towards the left, and negative velocity
    return -1;
  }
  return 0;
};

// ----- scroll ----- //

proto.onscroll = function() {
  let scroll = getScrollPosition();
  let scrollMoveX = this.pointerDownScroll.x - scroll.x;
  let scrollMoveY = this.pointerDownScroll.y - scroll.y;
  // cancel click/tap if scroll is too much
  if ( Math.abs( scrollMoveX ) > 3 || Math.abs( scrollMoveY ) > 3 ) {
    this.pointerDone();
  }
};

// ----- utils ----- //

function getScrollPosition() {
  return {
    x: window.pageXOffset,
    y: window.pageYOffset,
  };
}

// -----  ----- //

return Flickity;

} ) );
