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

// Props interface
interface Props {
	minScale?: number
	maxScale?: number
	zoom?: number
	resetTrigger?: 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,
	backgroundColor = 'transparent',
	pivot = 'cursor',
	zoomingElastic = true,
	limitTranslation = true,
	doubleClickToZoom = true,
	mouseWheelToZoom = true,
} = defineProps<Props>()

const emit = defineEmits(['update:zoom'])
// Reactive state using standard Vue composition API
const zoomElement = ref<HTMLElement | undefined>()
const containerWidth = ref(1)
const containerHeight = ref(1)
const containerLeft = ref(0)
const containerTop = ref(0)
const translateX = ref(0)
const animTranslateX = ref(0)
const translateY = ref(0)
const animTranslateY = ref(0)
const _zoom = ref(zoom ?? 1)
const scale = computed({
	get: () => zoom ?? _zoom.value,
	set: (val) => {
		_zoom.value = val
		emit('update:zoom', _zoom.value)
	},
})

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

// Momentum/inertia tracking
const velocityX = ref(0)
const velocityY = ref(0)
const lastMoveTime = ref(0)
const momentumRaf = ref<number | undefined>()

const wrapperStyle = computed(() => {
	// Translation is in normalized coordinates (0.5 = half container)
	// Convert to pixels
	const translateXValue = containerWidth.value * animTranslateX.value
	const translateYValue = containerHeight.value * animTranslateY.value
	return {
		transform: [
			`translate(${translateXValue}px, ${translateYValue}px)`,
			`scale(${animScale.value})`,
		].join(' '),
		transformOrigin: '50% 50%',
	}
})

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

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

defineExpose({ reset })

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

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

function onPointerMove(newMousePosX: number, newMousePosY: number) {
	if (isPointerDown.value) {
		const currentTime = Date.now()
		const pixelDeltaX = newMousePosX - pointerPosX.value
		const pixelDeltaY = newMousePosY - pointerPosY.value

		if (!panLocked.value) {
			// Calculate velocity for momentum
			const timeDelta = currentTime - lastMoveTime.value
			if (timeDelta > 0) {
				velocityX.value = pixelDeltaX / timeDelta
				velocityY.value = pixelDeltaY / timeDelta
			}

			const translateLimit = calcTranslateLimit()
			const maxTranslateX = translateLimit.x
			const maxTranslateY = translateLimit.y
			const newTranslateX = translateX.value + pixelDeltaX / containerWidth.value
			const newTranslateY = translateY.value + pixelDeltaY / containerHeight.value

			translateX.value = Math.max(-maxTranslateX, Math.min(maxTranslateX, newTranslateX))
			translateY.value = Math.max(-maxTranslateY, Math.min(maxTranslateY, newTranslateY))

			lastMoveTime.value = currentTime
		}
	}
	pointerPosX.value = newMousePosX
	pointerPosY.value = newMousePosY
}

function applyMomentum() {
	if (Math.abs(velocityX.value) < 0.01 && Math.abs(velocityY.value) < 0.01) {
		velocityX.value = 0
		velocityY.value = 0
		if (momentumRaf.value) {
			cancelAnimationFrame(momentumRaf.value)
			momentumRaf.value = undefined
		}
		return
	}

	const translateLimit = calcTranslateLimit()
	const newTranslateX = translateX.value + velocityX.value * 16 / containerWidth.value
	const newTranslateY = translateY.value + velocityY.value * 16 / containerHeight.value

	translateX.value = Math.max(-translateLimit.x, Math.min(translateLimit.x, newTranslateX))
	translateY.value = Math.max(-translateLimit.y, Math.min(translateLimit.y, newTranslateY))

	// Apply friction
	velocityX.value *= 0.95
	velocityY.value *= 0.95

	momentumRaf.value = requestAnimationFrame(applyMomentum)
}

const onInteractionEnd = useDebounceFn(() => {
	limit()
	panLocked.value = scale.value === 1

	// Start momentum animation if there's significant velocity
	if (!panLocked.value && (Math.abs(velocityX.value) > 0.1 || Math.abs(velocityY.value) > 0.1)) {
		if (momentumRaf.value) cancelAnimationFrame(momentumRaf.value)
		applyMomentum()
	} else {
		velocityX.value = 0
		velocityY.value = 0
	}
}, 50)

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

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

function calcTranslateLimit() {
	// When scale = 1, no panning needed (image fits in container)
	// When scale > 1, we can pan to see parts of the image outside the viewport

	// The image is scaled from center, so:
	// - At scale 2, the image is 2x larger
	// - The amount that extends beyond the container on EACH side is: (scale - 1) * containerSize / 2
	// - In normalized coordinates (where 1.0 = full container), this is: (scale - 1) / 2

	// Simple and correct approach:
	// Max pan distance in normalized coords = (scale - 1) / 2
	// This works for both X and Y directions regardless of aspect ratio

	const basePanLimit = Math.max(0, (scale.value - 1) / 2)

	return {
		x: basePanLimit,
		y: basePanLimit,
	}
}

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

function onMouseWheelDo(wheelDelta: number) {
	// More responsive zoom speed
	const scaleDelta = 1.15 ** (wheelDelta / 120)
	tryToScale(scaleDelta)
	onInteractionEnd()
}

function onMouseDown(ev: MouseEvent) {
	refreshContainerPos()
	isPointerDown.value = true
	pointerPosX.value = ev.clientX
	pointerPosY.value = ev.clientY
	lastMoveTime.value = Date.now()
	velocityX.value = 0
	velocityY.value = 0
	if (momentumRaf.value) {
		cancelAnimationFrame(momentumRaf.value)
		momentumRaf.value = undefined
	}
	document.addEventListener('mousemove', onMouseMove)
	document.addEventListener('mouseup', onMouseUp)
}

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

function onMouseMove(ev: MouseEvent) {
	// Prevent swiper from handling mouse drag when panning zoomed image
	if (!panLocked.value && scale.value > 1) {
		ev.stopPropagation()
	}
	onPointerMove(ev.clientX, ev.clientY)
}

function onTouchStart(ev: TouchEvent) {
	if (ev.touches.length === 1) {
		refreshContainerPos()
		pointerPosX.value = ev.touches[0].clientX
		pointerPosY.value = ev.touches[0].clientY
		lastMoveTime.value = Date.now()
		velocityX.value = 0
		velocityY.value = 0
		if (momentumRaf.value) {
			cancelAnimationFrame(momentumRaf.value)
			momentumRaf.value = undefined
		}
		isPointerDown.value = true
	} else if (ev.touches.length === 2) {
		if (momentumRaf.value) {
			cancelAnimationFrame(momentumRaf.value)
			momentumRaf.value = undefined
		}
		velocityX.value = 0
		velocityY.value = 0
		isPointerDown.value = true
		pointerPosX.value = (ev.touches[0].clientX + ev.touches[1].clientX) / 2
		pointerPosY.value = (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.value = Math.sqrt(distX * distX + distY * distY)
	}
	document.addEventListener('touchend', onTouchEnd)
}

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

function onTouchMove(ev: TouchEvent) {
	if (ev.touches.length === 1) {
		// Prevent swiper from handling touch when panning zoomed image
		if (!panLocked.value && scale.value > 1) {
			ev.stopPropagation()
		}
		onPointerMove(ev.touches[0].clientX, ev.touches[0].clientY)
	} else if (ev.touches.length === 2) {
		// Always prevent default during pinch zoom
		ev.stopPropagation()
		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.value)
		twoFingerInitDist.value = newTwoFingerDist
	}
}

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

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

function gainOn(from: number, to: number) {
	// Smoother easing with better responsiveness
	const delta = (to - from) * 0.4
	return Math.abs(delta) > 1e-5 ? from + delta : to
}

// Lifecycle hooks
onMounted(() => {
	tapDetector.value = new TapDetector()
	if (zoomElement.value) { tapDetector.value.attach(zoomElement.value) }
	if (doubleClickToZoom) {
		tapDetector.value.onDoubleTap(onDoubleTap as any)
		// Also add native dblclick event for mouse
		if (zoomElement.value) {
			zoomElement.value.addEventListener('dblclick', onDoubleTap as any)
		}
	}
	window.addEventListener('resize', onWindowResize)
	onWindowResize()
	refreshContainerPos()
	loop()
})

onUnmounted(() => {
	if (zoomElement.value) {
		tapDetector.value?.detach(zoomElement.value)
		if (doubleClickToZoom) {
			zoomElement.value.removeEventListener('dblclick', onDoubleTap as any)
		}
	}
	window.removeEventListener('resize', onWindowResize)
	if (raf.value) { window.cancelAnimationFrame(raf.value) }
	if (momentumRaf.value) { window.cancelAnimationFrame(momentumRaf.value) }
})

function onDoubleTap(ev: MouseEvent) {
	// Update pointer position if available
	if (ev.clientX > 0) {
		pointerPosX.value = ev.clientX
		pointerPosY.value = ev.clientY
	}

	// Calculate zoom levels
	const mediumZoom = Math.min(2.5, maxScale / 2 + 0.5)
	const highZoom = maxScale

	// Cycle through three states: 1x -> medium -> max -> 1x
	if (scale.value === 1) {
		// From no zoom -> medium zoom
		tryToScale(mediumZoom)
	} else if (scale.value < highZoom - 0.1) {
		// From medium zoom -> max zoom
		tryToScale(highZoom / scale.value)
	} else {
		// From max zoom -> no zoom (reset)
		reset()
	}
}

function onWindowResize() {
	if (zoomElement.value) {
		const styles = window.getComputedStyle(zoomElement.value)
		containerWidth.value = Number.parseFloat(styles.width)
		containerHeight.value = 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>
