import {
  extend,
  defer,
  requestAnimationFrame,
  prefixed,
} from "../../utils/core";
import { EVENTS, DOM_EVENTS } from "../../utils/constants";
import EventEmitter from "event-emitter";

// easing equations from https://github.com/danro/easing-js/blob/master/easing.js
const PI_D2 = Math.PI / 2;
const EASING_EQUATIONS = {
  easeOutSine: function (pos) {
    return Math.sin(pos * PI_D2);
  },
  easeInOutSine: function (pos) {
    return -0.5 * (Math.cos(Math.PI * pos) - 1);
  },
  easeInOutQuint: function (pos) {
    if ((pos /= 0.5) < 1) {
      return 0.5 * Math.pow(pos, 5);
    }
    return 0.5 * (Math.pow(pos - 2, 5) + 2);
  },
  easeInCubic: function (pos) {
    return Math.pow(pos, 3);
  },
};

class Snap {
  constructor(manager, options) {
    this.settings = extend(
      {
        duration: 80,
        minVelocity: 0.2,
        minDistance: 10,
        easing: EASING_EQUATIONS["easeInCubic"],
      },
      options || {}
    );

    this.supportsTouch = this.supportsTouch();

    if (this.supportsTouch) {
      this.setup(manager);
    }
  }

  setup(manager) {
    this.manager = manager;

    this.layout = this.manager.layout;

    this.fullsize = this.manager.settings.fullsize;
    if (this.fullsize) {
      this.element = this.manager.stage.element;
      this.scroller = window;
      this.disableScroll();
    } else {
      this.element = this.manager.stage.container;
      this.scroller = this.element;
      this.element.style["WebkitOverflowScrolling"] = "touch";
    }

    // this.overflow = this.manager.overflow;

    // set lookahead offset to page width
    this.manager.settings.offset = this.layout.width;
    this.manager.settings.afterScrolledTimeout = this.settings.duration * 2;

    this.isVertical = this.manager.settings.axis === "vertical";

    // disable snapping if not paginated or axis in not horizontal
    if (!this.manager.isPaginated || this.isVertical) {
      return;
    }

    this.touchCanceler = false;
    this.resizeCanceler = false;
    this.snapping = false;

    this.scrollLeft;
    this.scrollTop;

    this.startTouchX = undefined;
    this.startTouchY = undefined;
    this.startTime = undefined;
    this.endTouchX = undefined;
    this.endTouchY = undefined;
    this.endTime = undefined;

    this.addListeners();
  }

  supportsTouch() {
    if (
      "ontouchstart" in window ||
      (window.DocumentTouch && document instanceof DocumentTouch)
    ) {
      return true;
    }

    return false;
  }

  disableScroll() {
    this.element.style.overflow = "hidden";
  }

  enableScroll() {
    this.element.style.overflow = "";
  }

  addListeners() {
    this._onResize = this.onResize.bind(this);
    window.addEventListener("resize", this._onResize);

    this._onScroll = this.onScroll.bind(this);
    this.scroller.addEventListener("scroll", this._onScroll);

    this._onTouchStart = this.onTouchStart.bind(this);
    this.scroller.addEventListener("touchstart", this._onTouchStart, {
      passive: true,
    });
    this.on("touchstart", this._onTouchStart);

    this._onTouchMove = this.onTouchMove.bind(this);
    this.scroller.addEventListener("touchmove", this._onTouchMove, {
      passive: true,
    });
    this.on("touchmove", this._onTouchMove);

    this._onTouchEnd = this.onTouchEnd.bind(this);
    this.scroller.addEventListener("touchend", this._onTouchEnd, {
      passive: true,
    });
    this.on("touchend", this._onTouchEnd);

    this._afterDisplayed = this.afterDisplayed.bind(this);
    this.manager.on(EVENTS.MANAGERS.ADDED, this._afterDisplayed);
  }

  removeListeners() {
    window.removeEventListener("resize", this._onResize);
    this._onResize = undefined;

    this.scroller.removeEventListener("scroll", this._onScroll);
    this._onScroll = undefined;

    this.scroller.removeEventListener("touchstart", this._onTouchStart, {
      passive: true,
    });
    this.off("touchstart", this._onTouchStart);
    this._onTouchStart = undefined;

    this.scroller.removeEventListener("touchmove", this._onTouchMove, {
      passive: true,
    });
    this.off("touchmove", this._onTouchMove);
    this._onTouchMove = undefined;

    this.scroller.removeEventListener("touchend", this._onTouchEnd, {
      passive: true,
    });
    this.off("touchend", this._onTouchEnd);
    this._onTouchEnd = undefined;

    this.manager.off(EVENTS.MANAGERS.ADDED, this._afterDisplayed);
    this._afterDisplayed = undefined;
  }

  afterDisplayed(view) {
    let contents = view.contents;
    ["touchstart", "touchmove", "touchend"].forEach((e) => {
      contents.on(e, (ev) => this.triggerViewEvent(ev, contents));
    });
  }

  triggerViewEvent(e, contents) {
    this.emit(e.type, e, contents);
  }

  onScroll(e) {
    this.scrollLeft = this.fullsize ? window.scrollX : this.scroller.scrollLeft;
    this.scrollTop = this.fullsize ? window.scrollY : this.scroller.scrollTop;
  }

  onResize(e) {
    this.resizeCanceler = true;
  }

  onTouchStart(e) {
    let { screenX, screenY } = e.touches[0];

    if (this.fullsize) {
      this.enableScroll();
    }

    this.touchCanceler = true;

    if (!this.startTouchX) {
      this.startTouchX = screenX;
      this.startTouchY = screenY;
      this.startTime = this.now();
    }

    this.endTouchX = screenX;
    this.endTouchY = screenY;
    this.endTime = this.now();
  }

  onTouchMove(e) {
    let { screenX, screenY } = e.touches[0];
    let deltaY = Math.abs(screenY - this.endTouchY);

    this.touchCanceler = true;

    if (!this.fullsize && deltaY < 10) {
      this.element.scrollLeft -= screenX - this.endTouchX;
    }

    this.endTouchX = screenX;
    this.endTouchY = screenY;
    this.endTime = this.now();
  }

  onTouchEnd(e) {
    if (this.fullsize) {
      this.disableScroll();
    }

    this.touchCanceler = false;

    let swipped = this.wasSwiped();

    if (swipped !== 0) {
      this.snap(swipped);
    } else {
      this.snap();
    }

    this.startTouchX = undefined;
    this.startTouchY = undefined;
    this.startTime = undefined;
    this.endTouchX = undefined;
    this.endTouchY = undefined;
    this.endTime = undefined;
  }

  wasSwiped() {
    let snapWidth = this.layout.pageWidth * this.layout.divisor;
    let distance = this.endTouchX - this.startTouchX;
    let absolute = Math.abs(distance);
    let time = this.endTime - this.startTime;
    let velocity = distance / time;
    let minVelocity = this.settings.minVelocity;

    if (absolute <= this.settings.minDistance || absolute >= snapWidth) {
      return 0;
    }

    if (velocity > minVelocity) {
      // previous
      return -1;
    } else if (velocity < -minVelocity) {
      // next
      return 1;
    }
  }

  needsSnap() {
    let left = this.scrollLeft;
    let snapWidth = this.layout.pageWidth * this.layout.divisor;
    return left % snapWidth !== 0;
  }

  snap(howMany = 0) {
    let left = this.scrollLeft;
    let snapWidth = this.layout.pageWidth * this.layout.divisor;
    let snapTo = Math.round(left / snapWidth) * snapWidth;

    if (howMany) {
      snapTo += howMany * snapWidth;
    }

    return this.smoothScrollTo(snapTo);
  }

  smoothScrollTo(destination) {
    const deferred = new defer();
    const start = this.scrollLeft;
    const startTime = this.now();

    const duration = this.settings.duration;
    const easing = this.settings.easing;

    this.snapping = true;

    // add animation loop
    function tick() {
      const now = this.now();
      const time = Math.min(1, (now - startTime) / duration);
      const timeFunction = easing(time);

      if (this.touchCanceler || this.resizeCanceler) {
        this.resizeCanceler = false;
        this.snapping = false;
        deferred.resolve();
        return;
      }

      if (time < 1) {
        window.requestAnimationFrame(tick.bind(this));
        this.scrollTo(start + (destination - start) * time, 0);
      } else {
        this.scrollTo(destination, 0);
        this.snapping = false;
        deferred.resolve();
      }
    }

    tick.call(this);

    return deferred.promise;
  }

  scrollTo(left = 0, top = 0) {
    if (this.fullsize) {
      window.scroll(left, top);
    } else {
      this.scroller.scrollLeft = left;
      this.scroller.scrollTop = top;
    }
  }

  now() {
    return "now" in window.performance
      ? performance.now()
      : new Date().getTime();
  }

  destroy() {
    if (!this.scroller) {
      return;
    }

    if (this.fullsize) {
      this.enableScroll();
    }

    this.removeListeners();

    this.scroller = undefined;
  }
}

EventEmitter(Snap.prototype);

export default Snap;
