// Composables
import { useToggleScope } from '@/composables/toggleScope'
import { useVelocity } from '@/composables/touch'

// Utilities
import { computed, onBeforeUnmount, onMounted, onScopeDispose, shallowRef, watchEffect } from 'vue'
import { clamp } from '@/util'

// Types
import type { Ref } from 'vue'

export function useTouch ({
  el,
  isActive,
  isTemporary,
  width,
  touchless,
  position,
}: {
  el: Ref<HTMLElement | undefined>
  isActive: Ref<boolean>
  isTemporary: Ref<boolean>
  width: Ref<number>
  touchless: Ref<boolean>
  position: Ref<'left' | 'right' | 'top' | 'bottom'>
}) {
  onMounted(() => {
    window.addEventListener('touchstart', onTouchstart, { passive: true })
    window.addEventListener('touchmove', onTouchmove, { passive: false })
    window.addEventListener('touchend', onTouchend, { passive: true })
  })

  onBeforeUnmount(() => {
    window.removeEventListener('touchstart', onTouchstart)
    window.removeEventListener('touchmove', onTouchmove)
    window.removeEventListener('touchend', onTouchend)
  })

  const isHorizontal = computed(() => ['left', 'right'].includes(position.value))

  const { addMovement, endTouch, getVelocity } = useVelocity()
  let maybeDragging = false
  const isDragging = shallowRef(false)
  const dragProgress = shallowRef(0)
  const offset = shallowRef(0)
  let start: [number, number] | undefined

  function getOffset (pos: number, active: boolean): number {
    return (
      position.value === 'left' ? pos
      : position.value === 'right' ? document.documentElement.clientWidth - pos
      : position.value === 'top' ? pos
      : position.value === 'bottom' ? document.documentElement.clientHeight - pos
      : oops()
    ) - (active ? width.value : 0)
  }

  function getProgress (pos: number, limit = true): number {
    const progress = (
      position.value === 'left' ? (pos - offset.value) / width.value
      : position.value === 'right' ? (document.documentElement.clientWidth - pos - offset.value) / width.value
      : position.value === 'top' ? (pos - offset.value) / width.value
      : position.value === 'bottom' ? (document.documentElement.clientHeight - pos - offset.value) / width.value
      : oops()
    )
    return limit ? clamp(progress) : progress
  }

  function onTouchstart (e: TouchEvent) {
    if (touchless.value) return

    const touchX = e.changedTouches[0].clientX
    const touchY = e.changedTouches[0].clientY

    const touchZone = 25
    const inTouchZone: boolean =
      position.value === 'left' ? touchX < touchZone
      : position.value === 'right' ? touchX > document.documentElement.clientWidth - touchZone
      : position.value === 'top' ? touchY < touchZone
      : position.value === 'bottom' ? touchY > document.documentElement.clientHeight - touchZone
      : oops()

    const inElement: boolean = isActive.value && (
      position.value === 'left' ? touchX < width.value
      : position.value === 'right' ? touchX > document.documentElement.clientWidth - width.value
      : position.value === 'top' ? touchY < width.value
      : position.value === 'bottom' ? touchY > document.documentElement.clientHeight - width.value
      : oops()
    )

    if (
      inTouchZone ||
      inElement ||
      (isActive.value && isTemporary.value)
    ) {
      start = [touchX, touchY]

      offset.value = getOffset(isHorizontal.value ? touchX : touchY, isActive.value)
      dragProgress.value = getProgress(isHorizontal.value ? touchX : touchY)

      maybeDragging = offset.value > -20 && offset.value < 80
      endTouch(e)
      addMovement(e)
    }
  }

  function onTouchmove (e: TouchEvent) {
    const touchX = e.changedTouches[0].clientX
    const touchY = e.changedTouches[0].clientY

    if (maybeDragging) {
      if (!e.cancelable) {
        maybeDragging = false
        return
      }

      const dx = Math.abs(touchX - start![0])
      const dy = Math.abs(touchY - start![1])

      const thresholdMet = isHorizontal.value
        ? dx > dy && dx > 3
        : dy > dx && dy > 3

      if (thresholdMet) {
        isDragging.value = true
        maybeDragging = false
      } else if ((isHorizontal.value ? dy : dx) > 3) {
        maybeDragging = false
      }
    }

    if (!isDragging.value) return

    e.preventDefault()
    addMovement(e)

    const progress = getProgress(isHorizontal.value ? touchX : touchY, false)
    dragProgress.value = Math.max(0, Math.min(1, progress))

    if (progress > 1) {
      offset.value = getOffset(isHorizontal.value ? touchX : touchY, true)
    } else if (progress < 0) {
      offset.value = getOffset(isHorizontal.value ? touchX : touchY, false)
    }
  }

  function onTouchend (e: TouchEvent) {
    maybeDragging = false

    if (!isDragging.value) return

    addMovement(e)

    isDragging.value = false

    const velocity = getVelocity(e.changedTouches[0].identifier)
    const vx = Math.abs(velocity.x)
    const vy = Math.abs(velocity.y)
    const thresholdMet = isHorizontal.value
      ? vx > vy && vx > 400
      : vy > vx && vy > 3

    if (thresholdMet) {
      isActive.value = velocity.direction === ({
        left: 'right',
        right: 'left',
        top: 'down',
        bottom: 'up',
      }[position.value] || oops())
    } else {
      isActive.value = dragProgress.value > 0.5
    }
  }

  const dragStyles = computed(() => {
    return isDragging.value ? {
      transform:
        position.value === 'left' ? `translateX(calc(-100% + ${dragProgress.value * width.value}px))`
        : position.value === 'right' ? `translateX(calc(100% - ${dragProgress.value * width.value}px))`
        : position.value === 'top' ? `translateY(calc(-100% + ${dragProgress.value * width.value}px))`
        : position.value === 'bottom' ? `translateY(calc(100% - ${dragProgress.value * width.value}px))`
        : oops(),
      transition: 'none',
    } : undefined
  })

  useToggleScope(isDragging, () => {
    const transform = el.value?.style.transform ?? null
    const transition = el.value?.style.transition ?? null

    watchEffect(() => {
      el.value?.style.setProperty('transform', dragStyles.value?.transform || 'none')
      el.value?.style.setProperty('transition', dragStyles.value?.transition || null)
    })

    onScopeDispose(() => {
      el.value?.style.setProperty('transform', transform)
      el.value?.style.setProperty('transition', transition)
    })
  })

  return {
    isDragging,
    dragProgress,
    dragStyles,
  }
}

function oops (): never {
  throw new Error()
}
