<script lang="ts" setup>
import { useDebounceFn } from '@bagelink/vue'
import { onMounted, onUnmounted, watch } from 'vue'
import TapDetector from '../utils/tapDetector'

// Props interface
interface Props {
	minScale?: number
	maxScale?: number
	zoom?: number
	resetTrigger?: number
	aspectRatio?: number
	backgroundColor?: string
	pivot?: string // 'cursor' | 'image-center'
	zoomingElastic?: boolean
	limitTranslation?: boolean
	doubleClickToZoom?: boolean
	mouseWheelToZoom?: boolean
	disabled?: boolean
}

const {
	minScale = 1,
	maxScale = 5,
	zoom,
	disabled = false,
	aspectRatio = 1,
	backgroundColor = 'transparent',
	pivot = 'cursor',
	zoomingElastic = true,
	limitTranslation = true,
	doubleClickToZoom = true,
	mouseWheelToZoom = true,
} = defineProps<Props>()

const emit = defineEmits(['update:zoom'])
// Reactive state using $ref (vue-macros)
const zoomElement = $ref<HTMLElement | undefined>()
let containerWidth = $ref(1)
let containerHeight = $ref(1)
let containerLeft = $ref(0)
let containerTop = $ref(0)
let translateX = $ref(0)
let animTranslateX = $ref(0)
let translateY = $ref(0)
let animTranslateY = $ref(0)
let _zoom = $ref(zoom || 1)
let scale = $computed({
	get: () => zoom === undefined ? _zoom : zoom,
	set: (val) => {
		_zoom = val
		emit('update:zoom', _zoom)
	},
})

let animScale = $ref(1)
let lastFullWheelTime = $ref(0)
let lastWheelTime = $ref(0)
let lastWheelDirection = $ref<'x' | 'y'>('y')
let isPointerDown = $ref(false)
let pointerPosX = $ref(-1)
let pointerPosY = $ref(-1)
let twoFingerInitDist = $ref(0)
let panLocked = $ref(true)
let raf = $ref<number | undefined>()
let tapDetector = $ref<TapDetector | undefined>()

let translateXValue = $ref(0)
let translateYValue = $ref(0)

const wrapperStyle = $computed(() => {
	translateXValue = containerWidth * animTranslateX
	translateYValue = containerHeight * animTranslateY
	return {
		transform: [
			`translate(${translateXValue}px, ${translateYValue}px)`,
			`scale(${animScale})`,
		].join(' '),
	}
})

watch(() => scale, (newScale) => {
	const { x, y } = calcTranslateLimit()
	translateX = Math.max(-x, Math.min(x, translateX))
	translateY = Math.max(-y, Math.min(y, translateY))
	if (newScale !== 1) {
		panLocked = false
	}
})

function reset() {
	scale = 1
	panLocked = true
	translateX = 0
	translateY = 0
}

defineExpose({ reset })

function tryToScale(scaleDelta: number) {
	if (disabled) return
	let newScale = scale * scaleDelta
	if (zoomingElastic) {
		if (newScale < minScale! || newScale > maxScale!) {
			let log = Math.log2(scaleDelta)
			log *= 0.2
			scaleDelta = 2 ** log
			newScale = scale * scaleDelta
		}
	} else {
		if (newScale < minScale!) newScale = minScale!
		else if (newScale > maxScale!) newScale = maxScale!
	}
	scaleDelta = newScale / scale
	scale = newScale
	if (pivot !== 'image-center') {
		const normMousePosX = (pointerPosX - containerLeft) / containerWidth
		const normMousePosY = (pointerPosY - containerTop) / containerHeight
		translateX = (0.5 + translateX - normMousePosX) * scaleDelta + normMousePosX - 0.5
		translateY = (0.5 + translateY - normMousePosY) * scaleDelta + normMousePosY - 0.5
	}
}

function setPointerPosCenter() {
	pointerPosX = containerLeft + containerWidth / 2
	pointerPosY = containerTop + containerHeight / 2
}

function onPointerMove(newMousePosX: number, newMousePosY: number) {
	if (isPointerDown) {
		const pixelDeltaX = newMousePosX - pointerPosX
		const pixelDeltaY = newMousePosY - pointerPosY
		if (!panLocked) {
			const translateLimit = calcTranslateLimit()
			const maxTranslateX = translateLimit.x
			const maxTranslateY = translateLimit.y
			const newTranslateX = translateX + pixelDeltaX / containerWidth
			const newTranslateY = translateY + pixelDeltaY / containerHeight
			translateX = Math.max(-maxTranslateX, Math.min(maxTranslateX, newTranslateX))
			translateY = Math.max(-maxTranslateY, Math.min(maxTranslateY, newTranslateY))
		}
	}
	pointerPosX = newMousePosX
	pointerPosY = newMousePosY
}

const onInteractionEnd = useDebounceFn(() => {
	limit()
	panLocked = scale === 1
}, 100)

function limit() {
	if (scale < minScale!) {
		scale = minScale!
	} else if (scale > maxScale!) {
		tryToScale(maxScale! / scale)
	}

	if (limitTranslation) {
		const translateLimit = calcTranslateLimit()
		if (Math.abs(translateX) > translateLimit.x) {
			translateX *= translateLimit.x / Math.abs(translateX)
		}
		if (Math.abs(translateY) > translateLimit.y) {
			translateY *= translateLimit.y / Math.abs(translateY)
		}
	}
}

function calcTranslateLimit() {
	if (getMarginDirection() === 'y') {
		const imageToContainerRatio = containerWidth / aspectRatio / containerHeight
		let translateLimitY = (scale * imageToContainerRatio - 1) / 2
		if (translateLimitY < 0) translateLimitY = 0
		return {
			x: scale - 1, // Allow full movement to edges horizontally
			y: translateLimitY * 2 // Allow full movement to edges vertically
		}
	}
	const imageToContainerRatio = containerHeight * aspectRatio / containerWidth
	let translateLimitX = (scale * imageToContainerRatio - 1) / 2
	if (translateLimitX < 0) translateLimitX = 0
	return {
		x: translateLimitX * 2, // Allow full movement to edges horizontally
		y: scale - 1 // Allow full movement to edges vertically
	}
}

function getMarginDirection() {
	const containerRatio = containerWidth / containerHeight
	return containerRatio > aspectRatio ? 'x' : 'y'
}

function onMouseWheel(ev: WheelEvent) {
	if (!mouseWheelToZoom) return
	ev.preventDefault()
	const currTime = Date.now()
	if (Math.abs(ev.deltaY) === 120) {
		if (currTime - lastFullWheelTime > 50) {
			onMouseWheelDo(ev.deltaY)
			lastFullWheelTime = currTime
		}
	} else {
		if (currTime - lastWheelTime > 50) {
			lastWheelDirection = ev.deltaX > ev.deltaY ? 'x' : 'y'
			if (lastWheelDirection === 'y') {
				onMouseWheelDo(ev.deltaY)
			}
		}
		lastWheelTime = currTime
	}
}

function onMouseWheelDo(wheelDelta: number) {
	const scaleDelta = 1.25 ** (wheelDelta / 120)
	tryToScale(scaleDelta)
	onInteractionEnd()
}

function onMouseDown(ev: MouseEvent) {
	refreshContainerPos()
	isPointerDown = true
	pointerPosX = ev.clientX
	pointerPosY = ev.clientY
	document.addEventListener('mousemove', onMouseMove)
	document.addEventListener('mouseup', onMouseUp)
}

function onMouseUp() {
	isPointerDown = false
	onInteractionEnd()
	document.removeEventListener('mouseup', onMouseUp)
	document.removeEventListener('mousemove', onMouseMove)
}

function onMouseMove(ev: MouseEvent) {
	onPointerMove(ev.clientX, ev.clientY)
}

function onTouchStart(ev: TouchEvent) {
	if (ev.touches.length === 1) {
		refreshContainerPos()
		pointerPosX = ev.touches[0].clientX
		pointerPosY = ev.touches[0].clientY
		isPointerDown = true
	} else if (ev.touches.length === 2) {
		isPointerDown = true
		pointerPosX = (ev.touches[0].clientX + ev.touches[1].clientX) / 2
		pointerPosY = (ev.touches[0].clientY + ev.touches[1].clientY) / 2
		const distX = ev.touches[0].clientX - ev.touches[1].clientX
		const distY = ev.touches[0].clientY - ev.touches[1].clientY
		twoFingerInitDist = Math.sqrt(distX * distX + distY * distY)
	}
	document.addEventListener('touchend', onTouchEnd)
}

function onTouchEnd(ev: TouchEvent) {
	if (ev.touches.length === 0) {
		isPointerDown = false
		if (Math.abs(scale - 1) < 0.1) scale = 1
		onInteractionEnd()
	} else if (ev.touches.length === 1) {
		pointerPosX = ev.touches[0].clientX
		pointerPosY = ev.touches[0].clientY
	}
	document.removeEventListener('touchend', onTouchEnd)
}

function onTouchMove(ev: TouchEvent) {
	if (ev.touches.length === 1) {
		onPointerMove(ev.touches[0].clientX, ev.touches[0].clientY)
	} else if (ev.touches.length

		=== 2) {
		const distX = ev.touches[0].clientX - ev.touches[1].clientX
		const distY = ev.touches[0].clientY - ev.touches[1].clientY
		const newTwoFingerDist = Math.sqrt(distX * distX + distY * distY)
		tryToScale(newTwoFingerDist / twoFingerInitDist)
		twoFingerInitDist = newTwoFingerDist
	}
}

function refreshContainerPos() {
	if (zoomElement) {
		const rect = zoomElement.getBoundingClientRect()
		containerLeft = rect.left
		containerTop = rect.top
	}
}

function loop() {
	animScale = gainOn(animScale, scale)
	animTranslateX = gainOn(animTranslateX, translateX)
	animTranslateY = gainOn(animTranslateY, translateY)
	raf = window.requestAnimationFrame(loop)
}

function gainOn(from: number, to: number) {
	const delta = (to - from) * 0.3
	return Math.abs(delta) > 1e-5 ? from + delta : to
}

// Lifecycle hooks
onMounted(() => {
	tapDetector = new TapDetector()
	if (zoomElement) tapDetector.attach(zoomElement)
	if (doubleClickToZoom) {
		tapDetector.onDoubleTap(onDoubleTap as any)
	}
	window.addEventListener('resize', onWindowResize)
	onWindowResize()
	refreshContainerPos()
	loop()
})

onUnmounted(() => {
	if (zoomElement) tapDetector?.detach(zoomElement)
	window.removeEventListener('resize', onWindowResize)
	if (raf) window.cancelAnimationFrame(raf)
})

function onDoubleTap(ev: MouseEvent) {
	if (scale === 1) {
		if (ev.clientX > 0) {
			pointerPosX = ev.clientX
			pointerPosY = ev.clientY
		}
		tryToScale(Math.min(3, maxScale!))
	} else {
		reset()
	}
}

function onWindowResize() {
	if (zoomElement) {
		const styles = window.getComputedStyle(zoomElement)
		containerWidth = Number.parseFloat(styles.width)
		containerHeight = Number.parseFloat(styles.height)
		setPointerPosCenter()
		limit()
	}
}
</script>

<template>
	<div
		ref="zoomElement"
		class="vue-zoomer"
		:style="{ backgroundColor }"
		@mousewheel="onMouseWheel"
		@DOMMouseScroll="onMouseWheel"
		@mousedown="onMouseDown"
		@mouseout="setPointerPosCenter"
		@focusout="setPointerPosCenter"
		@touchstart="onTouchStart"
		@touchmove="onTouchMove"
	>
		<div class="zoomer" :style="wrapperStyle">
			<slot />
		</div>
	</div>
</template>

<style scoped>
.zoomer-debug {
	position: fixed;
	top: 0;
	right: 0;
	background-color: rgba(0, 0, 0, 0.5);
	color: white;
	z-index: 1000;
	height: 100px
}
.vue-zoomer {
  overflow: hidden;
}
.zoomer {
  transform-origin: 50% 50%;
  width: 100%;
  height: 100%;
  max-width: 100vw;
  max-height: 100vh;
}
.zoomer > img {
  vertical-align: top;
  user-select: none;
  -moz-user-drag: none;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  -webkit-user-drag: none;
  -moz-user-drag: none;
}
</style>
