<template>
  <uni-movable-view v-on="$listeners">
    <v-uni-resize-sensor @resize="setParent" />
    <slot />
  </uni-movable-view>
</template>
<script>
import touchtrack from 'uni-mixins/touchtrack'
import {
  Decline,
  Friction,
  STD
} from './utils'
import {
  initScrollBounce,
  disableScrollBounce
} from 'uni-platform/helpers/scroll'
var requesting = false

function _requestAnimationFrame (e) {
  if (!requesting) {
    requesting = true
    requestAnimationFrame(function () {
      e()
      requesting = false
    })
  }
}

function p (t, n) {
  if (t === n) {
    return 0
  }
  var i = t.offsetLeft
  return t.offsetParent ? (i += p(t.offsetParent, n)) : 0
}
function f (t, n) {
  if (t === n) {
    return 0
  }
  var i = t.offsetTop
  return t.offsetParent ? (i += f(t.offsetParent, n)) : 0
}
function v (a, b) {
  return +((1000 * a - 1000 * b) / 1000).toFixed(1)
}
function g (e, t, n) {
  var i = function (e) {
    if (e && e.id) {
      cancelAnimationFrame(e.id)
    }
    if (e) {
      e.cancelled = true
    }
  }
  var r = {
    id: 0,
    cancelled: false
  }
  function fn (n, i, r, o) {
    if (!n || !n.cancelled) {
      r(i)
      var a = e.done()
      if (!a) {
        if (!n.cancelled) {
          (n.id = requestAnimationFrame(fn.bind(null, n, i, r, o)))
        }
      }
      if (a && o) {
        o(i)
      }
    }
  }
  fn(r, e, t, n)
  return {
    cancel: i.bind(null, r),
    model: e
  }
}

export default {
  name: 'MovableView',
  mixins: [touchtrack],
  props: {
    direction: {
      type: String,
      default: 'none'
    },
    inertia: {
      type: [Boolean, String],
      default: false
    },
    outOfBounds: {
      type: [Boolean, String],
      default: false
    },
    x: {
      type: [Number, String],
      default: 0
    },
    y: {
      type: [Number, String],
      default: 0
    },
    damping: {
      type: [Number, String],
      default: 20
    },
    friction: {
      type: [Number, String],
      default: 2
    },
    disabled: {
      type: [Boolean, String],
      default: false
    },
    scale: {
      type: [Boolean, String],
      default: false
    },
    scaleMin: {
      type: [Number, String],
      default: 0.5
    },
    scaleMax: {
      type: [Number, String],
      default: 10
    },
    scaleValue: {
      type: [Number, String],
      default: 1
    },
    animation: {
      type: [Boolean, String],
      default: true
    }
  },
  data () {
    return {
      xSync: this._getPx(this.x),
      ySync: this._getPx(this.y),
      scaleValueSync: Number(this.scaleValue) || 1,
      width: 0,
      height: 0,
      minX: 0,
      minY: 0,
      maxX: 0,
      maxY: 0
    }
  },
  computed: {
    dampingNumber () {
      var val = Number(this.damping)
      return isNaN(val) ? 20 : val
    },
    frictionNumber () {
      var val = Number(this.friction)
      return isNaN(val) || val <= 0 ? 2 : val
    },
    scaleMinNumber () {
      var val = Number(this.scaleMin)
      return isNaN(val) ? 0.5 : val
    },
    scaleMaxNumber () {
      var val = Number(this.scaleMax)
      return isNaN(val) ? 10 : val
    },
    xMove () {
      return this.direction === 'all' || this.direction === 'horizontal'
    },
    yMove () {
      return this.direction === 'all' || this.direction === 'vertical'
    }
  },
  watch: {
    x (val) {
      this.xSync = this._getPx(val)
    },
    xSync (val) {
      this._setX(val)
    },
    y (val) {
      this.ySync = this._getPx(val)
    },
    ySync (val) {
      this._setY(val)
    },
    disabled () {
      this.__handleTouchStart()
    },
    scaleValue (val) {
      this.scaleValueSync = Number(val) || 0
    },
    scaleValueSync (val) {
      this._setScaleValue(val)
    },
    scaleMinNumber () {
      this._setScaleMinOrMax()
    },
    scaleMaxNumber () {
      this._setScaleMinOrMax()
    }
  },
  created: function () {
    this._offset = {
      x: 0,
      y: 0
    }
    this._scaleOffset = {
      x: 0,
      y: 0
    }
    this._translateX = 0
    this._translateY = 0
    this._scale = 1
    this._oldScale = 1

    this._STD = new STD(1, 9 * Math.pow(this.dampingNumber, 2) / 40, this.dampingNumber)
    this._friction = new Friction(1, this.frictionNumber)
    this._declineX = new Decline()
    this._declineY = new Decline()
    this.__touchInfo = {
      historyX: [0, 0],
      historyY: [0, 0],
      historyT: [0, 0]
    }
  },
  mounted: function () {
    this.touchtrack(this.$el, '_onTrack')
    this.setParent()
    this._friction.reconfigure(1, this.frictionNumber)
    this._STD.reconfigure(1, 9 * Math.pow(this.dampingNumber, 2) / 40, this.dampingNumber)
    this.$el.style.transformOrigin = 'center'
    initScrollBounce()
  },
  methods: {
    _getPx (val) {
      if (/\d+[ur]px$/i.test(val)) {
        return uni.upx2px(parseFloat(val))
      }
      return Number(val) || 0
    },
    _setX: function (val) {
      if (this.xMove) {
        if (val + this._scaleOffset.x === this._translateX) {
          return this._translateX
        } else {
          if (this._SFA) {
            this._SFA.cancel()
          }
          this._animationTo(val + this._scaleOffset.x, this.ySync + this._scaleOffset.y, this._scale)
        }
      }
      return val
    },
    _setY: function (val) {
      if (this.yMove) {
        if (val + this._scaleOffset.y === this._translateY) {
          return this._translateY
        } else {
          if (this._SFA) {
            this._SFA.cancel()
          }
          this._animationTo(this.xSync + this._scaleOffset.x, val + this._scaleOffset.y, this._scale)
        }
      }
      return val
    },
    _setScaleMinOrMax: function () {
      if (!this.scale) {
        return false
      }
      this._updateScale(this._scale, true)
      this._updateOldScale(this._scale)
    },
    _setScaleValue: function (scale) {
      if (!this.scale) {
        return false
      }
      scale = this._adjustScale(scale)
      this._updateScale(scale, true)
      this._updateOldScale(scale)
      return scale
    },
    __handleTouchStart: function () {
      if (!this._isScaling) {
        if (!this.disabled) {
          disableScrollBounce({
            disable: true
          })
          if (this._FA) {
            this._FA.cancel()
          }
          if (this._SFA) {
            this._SFA.cancel()
          }
          this.__touchInfo.historyX = [0, 0]
          this.__touchInfo.historyY = [0, 0]
          this.__touchInfo.historyT = [0, 0]
          if (this.xMove) {
            this.__baseX = this._translateX
          }
          if (this.yMove) {
            this.__baseY = this._translateY
          }
          this.$el.style.willChange = 'transform'
          this._checkCanMove = null
          this._firstMoveDirection = null
          this._isTouching = true
        }
      }
    },
    __handleTouchMove: function (event) {
      var self = this
      if (!this._isScaling && !this.disabled && this._isTouching) {
        let x = this._translateX
        let y = this._translateY
        if (this._firstMoveDirection === null) {
          this._firstMoveDirection = Math.abs(event.detail.dx / event.detail.dy) > 1 ? 'htouchmove' : 'vtouchmove'
        }
        if (this.xMove) {
          x = event.detail.dx + this.__baseX
          this.__touchInfo.historyX.shift()
          this.__touchInfo.historyX.push(x)
          if (!this.yMove && this._checkCanMove === null) {
            this._checkCanMove = Math.abs(event.detail.dx / event.detail.dy) < 1
          }
        }
        if (this.yMove) {
          y = event.detail.dy + this.__baseY
          this.__touchInfo.historyY.shift()
          this.__touchInfo.historyY.push(y)
          if (!this.xMove && this._checkCanMove === null) {
            this._checkCanMove = Math.abs(event.detail.dy / event.detail.dx) < 1
          }
        }
        this.__touchInfo.historyT.shift()
        this.__touchInfo.historyT.push(event.detail.timeStamp)

        if (!this._checkCanMove) {
          event.preventDefault()
          let source = 'touch'
          if (x < this.minX) {
            if (this.outOfBounds) {
              source = 'touch-out-of-bounds'
              x = this.minX - this._declineX.x(this.minX - x)
            } else {
              x = this.minX
            }
          } else if (x > this.maxX) {
            if (this.outOfBounds) {
              source = 'touch-out-of-bounds'
              x = this.maxX + this._declineX.x(x - this.maxX)
            } else {
              x = this.maxX
            }
          }
          if (y < this.minY) {
            if (this.outOfBounds) {
              source = 'touch-out-of-bounds'
              y = this.minY - this._declineY.x(this.minY - y)
            } else {
              y = this.minY
            }
          } else {
            if (y > this.maxY) {
              if (this.outOfBounds) {
                source = 'touch-out-of-bounds'
                y = this.maxY + this._declineY.x(y - this.maxY)
              } else {
                y = this.maxY
              }
            }
          }
          _requestAnimationFrame(function () {
            self._setTransform(x, y, self._scale, source)
          })
        }
      }
    },
    __handleTouchEnd: function () {
      var self = this
      if (!this._isScaling && !this.disabled && this._isTouching) {
        disableScrollBounce({
          disable: false
        })
        this.$el.style.willChange = 'auto'
        this._isTouching = false
        if (!this._checkCanMove && !this._revise('out-of-bounds') && this.inertia) {
          const xv = 1000 * (this.__touchInfo.historyX[1] - this.__touchInfo.historyX[0]) / (this.__touchInfo.historyT[1] - this.__touchInfo.historyT[0])
          const yv = 1000 * (this.__touchInfo.historyY[1] - this.__touchInfo.historyY[0]) / (this.__touchInfo.historyT[1] - this.__touchInfo.historyT[0])
          this._friction.setV(xv, yv)
          this._friction.setS(this._translateX, this._translateY)
          const x0 = this._friction.delta().x
          const y0 = this._friction.delta().y
          let x = x0 + this._translateX
          let y = y0 + this._translateY
          if (x < this.minX) {
            x = this.minX
            y = this._translateY + (this.minX - this._translateX) * y0 / x0
          } else {
            if (x > this.maxX) {
              x = this.maxX
              y = this._translateY + (this.maxX - this._translateX) * y0 / x0
            }
          }
          if (y < this.minY) {
            y = this.minY
            x = this._translateX + (this.minY - this._translateY) * x0 / y0
          } else {
            if (y > this.maxY) {
              y = this.maxY
              x = this._translateX + (this.maxY - this._translateY) * x0 / y0
            }
          }
          this._friction.setEnd(x, y)
          this._FA = g(this._friction, function () {
            var t = self._friction.s()
            var x = t.x
            var y = t.y
            self._setTransform(x, y, self._scale, 'friction')
          }, function () {
            self._FA.cancel()
          })
        }
      }
    },
    _onTrack: function (event) {
      switch (event.detail.state) {
        case 'start':
          this.__handleTouchStart()
          break
        case 'move':
          this.__handleTouchMove(event)
          break
        case 'end':
          this.__handleTouchEnd()
      }
    },
    _getLimitXY: function (x, y) {
      var outOfBounds = false
      if (x > this.maxX) {
        x = this.maxX
        outOfBounds = true
      } else {
        if (x < this.minX) {
          x = this.minX
          outOfBounds = true
        }
      }
      if (y > this.maxY) {
        y = this.maxY
        outOfBounds = true
      } else {
        if (y < this.minY) {
          y = this.minY
          outOfBounds = true
        }
      }
      return {
        x,
        y,
        outOfBounds
      }
    },
    setParent: function () {
      if (!this.$parent._isMounted) {
        return
      }
      if (this._FA) {
        this._FA.cancel()
      }
      if (this._SFA) {
        this._SFA.cancel()
      }
      var scale = this.scale ? this.scaleValueSync : 1
      this._updateOffset()
      this._updateWH(scale)
      this._updateBoundary()
      this._translateX = this.xSync + this._scaleOffset.x
      this._translateY = this.ySync + this._scaleOffset.y
      var limitXY = this._getLimitXY(this._translateX, this._translateY)
      var x = limitXY.x
      var y = limitXY.y
      this._setTransform(x, y, scale, '', true)
      this._updateOldScale(scale)
    },
    _updateOffset: function () {
      this._offset.x = p(this.$el, this.$parent.$el)
      this._offset.y = f(this.$el, this.$parent.$el)
    },
    _updateWH: function (scale) {
      scale = scale || this._scale
      scale = this._adjustScale(scale)
      var rect = this.$el.getBoundingClientRect()
      this.height = rect.height / this._scale
      this.width = rect.width / this._scale
      var height = this.height * scale
      var width = this.width * scale
      this._scaleOffset.x = (width - this.width) / 2
      this._scaleOffset.y = (height - this.height) / 2
    },
    _updateBoundary: function () {
      var x = 0 - this._offset.x + this._scaleOffset.x
      var width = this.$parent.width - this.width - this._offset.x - this._scaleOffset.x
      this.minX = Math.min(x, width)
      this.maxX = Math.max(x, width)
      var y = 0 - this._offset.y + this._scaleOffset.y
      var height = this.$parent.height - this.height - this._offset.y - this._scaleOffset.y
      this.minY = Math.min(y, height)
      this.maxY = Math.max(y, height)
    },
    _beginScale: function () {
      this._isScaling = true
    },
    _endScale: function () {
      this._isScaling = false
      this._updateOldScale(this._scale)
    },
    _setScale: function (scale) {
      if (this.scale) {
        scale = this._oldScale * scale
        this._beginScale()
        this._updateScale(scale)
      }
    },
    _updateScale: function (scale, animat) {
      var self = this
      if (this.scale) {
        scale = this._adjustScale(scale)
        this._updateWH(scale)
        this._updateBoundary()
        const limitXY = this._getLimitXY(this._translateX, this._translateY)
        const x = limitXY.x
        const y = limitXY.y
        if (animat) {
          this._animationTo(x, y, scale, '', true, true)
        } else {
          _requestAnimationFrame(function () {
            self._setTransform(x, y, scale, '', true, true)
          })
        }
      }
    },
    _updateOldScale: function (scale) {
      this._oldScale = scale
    },
    _adjustScale: function (scale) {
      scale = Math.max(0.5, this.scaleMinNumber, scale)
      scale = Math.min(10, this.scaleMaxNumber, scale)
      return scale
    },
    _animationTo: function (x, y, scale, source, r, o) {
      var self = this
      if (this._FA) {
        this._FA.cancel()
      }
      if (this._SFA) {
        this._SFA.cancel()
      }
      if (!this.xMove) {
        x = this._translateX
      }
      if (!this.yMove) {
        y = this._translateY
      }
      if (!this.scale) {
        scale = this._scale
      }
      var limitXY = this._getLimitXY(x, y)
      x = limitXY.x
      y = limitXY.y
      if (!this.animation) {
        this._setTransform(x, y, scale, source, r, o)
        return
      }
      this._STD._springX._solution = null
      this._STD._springY._solution = null
      this._STD._springScale._solution = null
      this._STD._springX._endPosition = this._translateX
      this._STD._springY._endPosition = this._translateY
      this._STD._springScale._endPosition = this._scale
      this._STD.setEnd(x, y, scale, 1)
      this._SFA = g(this._STD, function () {
        var data = self._STD.x()
        var x = data.x
        var y = data.y
        var scale = data.scale
        self._setTransform(x, y, scale, source, r, o)
      }, function () {
        self._SFA.cancel()
      })
    },
    _revise: function (source) {
      var limitXY = this._getLimitXY(this._translateX, this._translateY)
      var x = limitXY.x
      var y = limitXY.y
      var outOfBounds = limitXY.outOfBounds
      if (outOfBounds) {
        this._animationTo(x, y, this._scale, source)
      }
      return outOfBounds
    },
    _setTransform: function (x, y, scale, source = '', r, o) {
      if (!(x !== null && x.toString() !== 'NaN' && typeof x === 'number')) {
        x = this._translateX || 0
      }
      if (!(y !== null && y.toString() !== 'NaN' && typeof y === 'number')) {
        y = this._translateY || 0
      }
      x = Number(x.toFixed(1))
      y = Number(y.toFixed(1))
      scale = Number(scale.toFixed(1))
      if (!(this._translateX === x && this._translateY === y)) {
        if (!r) {
          this.$trigger('change', {}, {
            x: v(x, this._scaleOffset.x),
            y: v(y, this._scaleOffset.y),
            source: source
          })
        }
      }
      if (!this.scale) {
        scale = this._scale
      }
      scale = this._adjustScale(scale)
      scale = +scale.toFixed(3)
      if (o && scale !== this._scale) {
        this.$trigger('scale', {}, {
          x: x,
          y: y,
          scale: scale
        })
      }
      var transform = 'translateX(' + x + 'px) translateY(' + y + 'px) translateZ(0px) scale(' + scale + ')'
      this.$el.style.transform = transform
      this.$el.style.webkitTransform = transform
      this._translateX = x
      this._translateY = y
      this._scale = scale
    }
  }
}
</script>
<style>
uni-movable-view {
  display: inline-block;
  width: 10px;
  height: 10px;
  top: 0px;
  left: 0px;
  position: absolute;
  cursor: grab;
}

uni-movable-view[hidden] {
  display: none;
}
</style>
