<template>
  <div
    class="md-scroll-view"
    @touchstart="$_onScrollerTouchStart"
    @touchmove="$_onScrollerTouchMove"
    @touchend="$_onScrollerTouchEnd"
    @touchcancel="$_onScrollerTouchEnd"
    @mousedown="$_onScrollerMouseDown"
    @mousemove="$_onScrollerMouseMove"
    @mouseup="$_onScrollerMouseUp"
    @mouseleave="$_onScrollerMouseUp"
  >
    <div class="scroll-view-header" v-if="$slots.header">
      <slot name="header"></slot>
    </div>
    <div
      class="scroll-view-container"
      :class="{
        'horizon': scrollingX && !scrollingY
      }"
      scroll-wrapper
    >
      <div
        v-if="hasRefresher"
        class="scroll-view-refresh"
        :class="{
          'refreshing': isRefreshing,
          'refresh-active': isRefreshActive,
        }"
      >
        <slot
          name="refresh"
          :scroll-top="scrollY"
          :is-refreshing="isRefreshing"
          :is-refresh-active="isRefreshActive"
        ></slot>
      </div>
      <slot></slot>
      <div
        v-if="hasMore"
        :class="{active: isEndReachingStart || isEndReaching}"
        class="scroll-view-more"
      >
        <slot name="more" :is-end-reaching="isEndReachingStart || isEndReaching"></slot>
      </div>
    </div>
    <div class="scroll-view-footer" v-if="$slots.footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

<script>
import {debounce} from '../_util'
import Scroller from '../_util/scroller'
import {render} from '../_util/render'

export default {
  name: 'md-scroll-view',
  props: {
    scrollingX: {
      type: Boolean,
      default: true,
    },
    scrollingY: {
      type: Boolean,
      default: true,
    },
    bouncing: {
      type: Boolean,
      default: true,
    },
    autoReflow: {
      type: Boolean,
      default: false,
    },
    manualInit: {
      type: Boolean,
      default: false,
    },
    endReachedThreshold: {
      type: Number,
      default: 0,
    },
    immediateCheckEndReaching: {
      type: Boolean,
      default: false,
    },
    touchAngle: {
      type: Number,
      default: 45,
    },
    isPrevent: {
      type: Boolean,
      default: true,
    },
  },
  data() {
    return {
      container: null,
      content: null,
      refresher: null,
      more: null,
      scroller: null,
      refreshOffsetY: 0,
      isInitialed: false,
      isMouseDown: false,
      isRefreshing: false,
      isRefreshActive: false,
      isEndReachingStart: false,
      isEndReaching: false,
      scrollX: null,
      scrollY: null,
      startX: 0,
      startY: 0,
      currentX: 0,
      currentY: 0,
      containerW: 0,
      containerH: 0,
      contentW: 0,
      contentH: 0,
      reflowTimer: null,
      endReachedHandler: null,
    }
  },
  computed: {
    hasRefresher() {
      return !!(this.$slots.refresh || this.$scopedSlots.refresh)
    },
    hasMore() {
      return !!(this.$slots.more || this.$scopedSlots.more)
    },
  },
  watch: {
    autoReflow(val) {
      if (val) {
        this.$_initAutoReflow()
      } else {
        this.$_destroyAutoReflow()
      }
    },
  },
  mounted() {
    if (!this.manualInit) {
      this.$_initScroller()
    }
  },
  destroyed() {
    this.$_destroyAutoReflow()
  },
  methods: {
    $_initScroller() {
      /* istanbul ignore if */
      if (this.isInitialed) {
        return
      }
      this.container = this.$el
      this.refresher = this.$el.querySelector('.scroll-view-refresh')
      this.more = this.$el.querySelector('.scroll-view-more')
      this.content = this.$el.querySelector('.scroll-view-container')
      this.refreshOffsetY = this.refresher ? this.refresher.clientHeight : 0
      this.moreOffsetY = this.more ? this.more.clientHeight : 0
      const container = this.container
      const content = this.content
      const rect = container.getBoundingClientRect()
      const scroller = new Scroller(
        (left, top) => {
          render(content, left, top)
          if (this.isInitialed) {
            this.$_onScroll(left, top)
          }
        },
        {
          scrollingX: this.scrollingX,
          scrollingY: this.scrollingY,
          bouncing: this.bouncing,
          zooming: false,
          animationDuration: 200,
          speedMultiplier: 1.2,
          inRequestAnimationFrame: true,
        },
      )
      scroller.setPosition(rect.left + container.clientLeft, rect.top + container.clientTop)
      if (this.hasRefresher) {
        scroller.activatePullToRefresh(
          this.refreshOffsetY,
          () => {
            this.isRefreshActive = true
            this.isRefreshing = false
            this.$emit('refreshActive')
          },
          () => {
            this.isRefreshActive = false
            this.isRefreshing = false
          },
          () => {
            this.isRefreshActive = false
            this.isRefreshing = true
            this.$emit('refreshing')
          },
        )
      }
      this.scroller = scroller
      this.reflowScroller(true)
      this.autoReflow && this.$_initAutoReflow()
      this.endReachedHandler = debounce(() => {
        this.isEndReaching = true
        this.$emit('endReached')
        this.$emit('end-reached')
      }, 50)

      setTimeout(() => {
        this.isInitialed = true
      }, 50)

      if (this.immediateCheckEndReaching) {
        this.$nextTick(this.$_checkScrollerEnd)
      }
    },
    $_initAutoReflow() {
      this.$_destroyAutoReflow()
      this.reflowTimer = setInterval(() => {
        this.reflowScroller()
      }, 100)
    },
    $_destroyAutoReflow() {
      this.reflowTimer && clearInterval(this.reflowTimer)
    },
    $_checkScrollerEnd() {
      if (!this.scroller) {
        return
      }
      const containerHeight = this.scroller._clientHeight
      const content = this.scroller._contentHeight
      const top = this.scroller._scrollTop
      const moreOffsetY = this.moreOffsetY
      const moreThreshold = this.endReachedThreshold
      const endOffset = content - containerHeight - (top + moreOffsetY + moreThreshold)
      if (top >= 0 && !this.isEndReaching && endOffset <= 0 && this.endReachedHandler) {
        // First prepare for "load more" state
        this.isEndReachingStart = true
        // Second enter "load more" state
        // & trigger endReached event only once after the rebounding animation
        this.endReachedHandler()
      }
    },
    $_getScrollerAngle() {
      const diffX = this.currentX - this.startX
      const diffY = this.currentY - this.startY
      const angle = Math.atan2(Math.abs(diffY), Math.abs(diffX)) * 180 / Math.PI
      return this.scrollingX ? 90 - angle : angle
    },
    // MARK: events handler
    $_onScrollerTouchStart(event) {
      // event.target.tagName && event.target.tagName.match(/input|textarea|select/i)
      /* istanbul ignore if */
      if (!this.scroller) {
        return
      }
      this.startX = event.targetTouches[0].pageX
      this.startY = event.targetTouches[0].pageY
      this.scroller.doTouchStart(event.touches, event.timeStamp)
    },
    $_onScrollerTouchMove(event) {
      /* istanbul ignore if */
      if (!this.scroller) {
        return
      }
      let hadPrevent = false

      if (this.isPrevent) {
        event.preventDefault()

        hadPrevent = true
      }

      this.currentX = event.targetTouches[0].pageX
      this.currentY = event.targetTouches[0].pageY

      if (!this.scrollingX || !this.scrollingY) {
        const currentTouchAngle = this.$_getScrollerAngle()
        if (currentTouchAngle < this.touchAngle) {
          return
        }
      }

      if (!hadPrevent && event.cancelable) {
        event.preventDefault()
      }

      this.scroller.doTouchMove(event.touches, event.timeStamp, event.scale)

      const boundaryDistance = 15
      const scrollLeft = document.documentElement.scrollLeft || window.pageXOffset || document.body.scrollLeft
      const scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop

      const pX = this.currentX - scrollLeft
      const pY = this.currentY - scrollTop
      if (
        pX > document.documentElement.clientWidth - boundaryDistance ||
        pY > document.documentElement.clientHeight - boundaryDistance ||
        pX < boundaryDistance ||
        pY < boundaryDistance
      ) {
        this.scroller.doTouchEnd(event.timeStamp)
      }
    },
    $_onScrollerTouchEnd(event) {
      /* istanbul ignore if */
      if (!this.scroller) {
        return
      }
      this.scroller.doTouchEnd(event.timeStamp)
    },
    $_onScrollerMouseDown(event) {
      /* istanbul ignore if */
      if (!this.scroller) {
        return
      }
      this.startX = event.pageX
      this.startY = event.pageY
      this.scroller.doTouchStart(
        [
          {
            pageX: event.pageX,
            pageY: event.pageY,
          },
        ],
        event.timeStamp,
      )
      this.isMouseDown = true
    },
    $_onScrollerMouseMove(event) {
      /* istanbul ignore if */
      if (!this.scroller || !this.isMouseDown) {
        return
      }

      this.currentX = event.pageX
      this.currentY = event.pageY
      if (!this.scrollingX || !this.scrollingY) {
        const currentTouchAngle = this.$_getScrollerAngle()
        if (currentTouchAngle < this.touchAngle) {
          return
        }
      }
      this.scroller.doTouchMove(
        [
          {
            pageX: event.pageX,
            pageY: event.pageY,
          },
        ],
        event.timeStamp,
      )
      this.isMouseDown = true
    },
    $_onScrollerMouseUp(event) {
      /* istanbul ignore if */
      if (!this.scroller || !this.isMouseDown) {
        return
      }
      this.scroller.doTouchEnd(event.timeStamp)
      this.isMouseDown = false
    },
    $_onScroll(left, top) {
      left = +left.toFixed(2)
      top = +top.toFixed(2)
      if (this.scrollX === left && this.scrollY === top) {
        return
      }
      this.scrollX = left
      this.scrollY = top
      this.$_checkScrollerEnd()
      this.$emit('scroll', {scrollLeft: left, scrollTop: top})
    },
    init() {
      this.$nextTick(() => {
        this.$_initScroller()
      })
    },
    scrollTo(left, top, animate = false) {
      /* istanbul ignore if */
      if (!this.scroller) {
        return
      }
      this.scroller.scrollTo(left, top, animate)
    },
    getOffsets() {
      /* istanbul ignore if */
      if (!this.scroller) {
        return {left: 0, top: 0}
      }
      return this.scroller.getValues()
    },
    reflowScroller(force = false) {
      const container = this.container
      const content = this.content
      /* istanbul ignore if */
      if (!this.scroller || !container || !content) {
        return
      }
      this.$nextTick(() => {
        const containerW = container.clientWidth
        const containerH = container.clientHeight
        const contentW = content.offsetWidth
        const contentH = content.offsetHeight

        if (
          force ||
          this.containerW !== containerW ||
          this.containerH !== containerH ||
          this.contentW !== contentW ||
          this.contentH !== contentH
        ) {
          this.scroller.setDimensions(
            container.clientWidth,
            container.clientHeight,
            content.offsetWidth,
            content.offsetHeight,
          )
          this.containerW = containerW
          this.containerH = containerH
          this.contentW = contentW
          this.contentH = contentH
        }
      })
    },
    triggerRefresh() {
      /* istanbul ignore if */
      if (!this.scroller) {
        return
      }
      this.scroller.triggerPullToRefresh()
    },
    finishRefresh() {
      /* istanbul ignore if */
      if (!this.scroller) {
        return
      }
      this.scroller.finishPullToRefresh()
      this.reflowScroller()
    },
    finishLoadMore() {
      /* istanbul ignore if */
      if (!this.scroller) {
        return
      }
      this.isEndReachingStart = false
      this.isEndReaching = false
      this.reflowScroller()
    },
  },
}

</script>

<style lang="stylus">
.md-scroll-view
  position relative
  display block
  height 100%
  overflow hidden
  user-select none
  .scroll-view-header, .scroll-view-footer
    position absolute
    left 0
    right 0
    z-index 2
  .scroll-view-header
    top 0
  .scroll-view-footer
    bottom 0
  .scroll-view-container
    clearfix()
    position relative
    z-index 1
    // display inline-block
    .scroll-view-refresh
      clearfix()
      position absolute
      left 0
      right 0
      transform translate3d(0, -100%, 0)
    .scroll-view-more
      visibility hidden
      &.active
        visibility visible
    &.horizon
      display inline-block
</style>
