<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'

const props = defineProps({
	autoHeight: { type: Boolean, default: false },
	allowScroll: { type: Boolean, default: true },
	freeDrag: { type: Boolean, default: true },
	items: { type: Number, default: 4 },
	index: { type: Number, default: 0 },
	rtl: { type: Boolean, default: false },
	autoplay: { type: Boolean, default: false },
	autoPlaySpeed: { type: Number, default: 4000 },
	dots: { type: Boolean, default: false },
})

const emit = defineEmits(['update:index'])

// Constants
const ANIMATION_TIMINGS = {
	TOUCH: 150,
	MOBILE: 420,
	DESKTOP: 840,
	MAX_DURATION: 300,
	BASE_DURATION: 150,
	DURATION_PER_PANEL: 50,
} as const

const THRESHOLDS = {
	DRAG: 20,
	TOUCH: 5,
	SWIPE_PERCENT: 0.1,
	VELOCITY: 0.15,
	WHEEL_PERCENT: 0.3,
} as const

// Add gap constant
const GAP_PERCENT = 1

// Template refs and state
const bglSlider = ref<HTMLElement | undefined>()
const isSliderAvailable = computed(() => !!bglSlider.value)

const itemCount = ref(props.items)
const activeSlideIndex = ref(props.index)
const slideCount = ref(0)
const yHeight = ref('auto')

// Interaction state
const isDragging = ref(false)
const isPressed = ref(false)
const startX = ref(0)
const startY = ref(0)
const translateX = ref(0)
const lastX = ref(0)
const lastTime = ref(0)
const accumulatedDeltaX = ref(0)

let timeout: NodeJS.Timeout | undefined
let wheelTimeout: NodeJS.Timeout | undefined
let autoPlayInterval: NodeJS.Timeout | undefined

// Add interface for velocity tracking
interface VelocitySample {
	time: number
	position: number
}

const velocityTracker = ref<VelocitySample[]>([])
const VELOCITY_SAMPLE_DURATION = 100 // ms to track velocity

function getAverageVelocity(): number {
	if (velocityTracker.value.length < 2) { return 0 }

	const now = Date.now()
	// Only consider samples within our sample duration
	const recentSamples = velocityTracker.value.filter((sample: VelocitySample) => now - sample.time < VELOCITY_SAMPLE_DURATION)

	if (recentSamples.length < 2) { return 0 }

	const [first] = recentSamples
	const last = recentSamples[recentSamples.length - 1]
	const timeDelta = last.time - first.time

	if (timeDelta === 0) { return 0 }

	return (last.position - first.position) / timeDelta
}

// Transform helpers
function getCurrentTransform(): number {
	if (!bglSlider.value) { return 0 }
	const { transform } = bglSlider.value.style
	const value = transform ? Number.parseInt(transform.replace(/[^-\d.]/g, ''), 10) : 0
	return props.rtl ? -value : value
}

function setTransform(value: number) {
	if (!bglSlider.value) { return }
	const rtlValue = props.rtl ? -value : value
	bglSlider.value.style.transform = `translateX(${rtlValue}px)`
}

function easeInOutQuad(t: number) {
	return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
}

function animateTransform(start: number, end: number, duration: number) {
	const startTime = performance.now()

	function animate(currentTime: number) {
		if (!isSliderAvailable.value) { return }
		const timeElapsed = currentTime - startTime
		const progress = Math.min(timeElapsed / duration, 1)
		const currentTransform = start + (end - start) * easeInOutQuad(progress)
		setTransform(currentTransform)
		if (progress < 1) { requestAnimationFrame(animate) }
	}

	requestAnimationFrame(animate)
}

// Navigation
function goToSlide(index: number, isTouchNav = false) {
	countSlides()
	if (!isSliderAvailable.value || !bglSlider.value || index < 0 || index >= slideCount.value) { return }

	const containerWidth = bglSlider.value.offsetWidth
	const gapWidth = (containerWidth * GAP_PERCENT) / 100
	const totalGapWidth = (itemCount.value - 1) * gapWidth
	const singleItemWidth = (containerWidth - totalGapWidth) / itemCount.value

	// Account for all gaps before this slide
	const gapsBeforeSlide = index * gapWidth
	const targetTransform = -(singleItemWidth * index + gapsBeforeSlide)

	const currentTransform = getCurrentTransform()
	const duration = isTouchNav
		? ANIMATION_TIMINGS.TOUCH
		: window.innerWidth < 600
			? ANIMATION_TIMINGS.MOBILE
			: ANIMATION_TIMINGS.DESKTOP

	animateTransform(currentTransform, targetTransform, duration)
	activeSlideIndex.value = index
	updateHeight()
}

function next() {
	if (!isSliderAvailable.value) { return }
	countSlides()
	// In RTL, next and prev are reversed
	const nextIndex = props.rtl
		? (activeSlideIndex.value - 1 + slideCount.value) % slideCount.value
		: (activeSlideIndex.value + 1) % slideCount.value
	goToSlide(nextIndex)
}

function prev() {
	if (!isSliderAvailable.value) { return }
	countSlides()
	// In RTL, next and prev are reversed
	const prevIndex = props.rtl
		? (activeSlideIndex.value + 1) % slideCount.value
		: (activeSlideIndex.value - 1 + slideCount.value) % slideCount.value
	goToSlide(prevIndex)
}

// Height management
function calcHeight() {
	if (!isSliderAvailable.value || !bglSlider.value) { return }
	try {
		const activeSlide = bglSlider.value.children[activeSlideIndex.value]
		if (!activeSlide || !(activeSlide instanceof HTMLElement)) { return }
		const children = Array.from(activeSlide.children) as HTMLElement[]
		const totalHeight = children.reduce((sum, el) => sum + el.clientHeight, 0)
		yHeight.value = `${totalHeight}px`
	} catch (error) {
		console.error('Error calculating height:', error)
	}
}

function updateHeight() {
	if (!props.autoHeight) { return }
	setTimeout(calcHeight, 200)
}

// Slide management
function countSlides() {
	if (!isSliderAvailable.value || !bglSlider.value) { return }
	slideCount.value = bglSlider.value.children.length
}

// Event handlers
function handleSlideChange() {
	if (props.index !== activeSlideIndex.value) {
		emit('update:index', activeSlideIndex.value)
	}
}

function handleResize() {
	if (!isSliderAvailable.value || !bglSlider.value) { return }
	itemCount.value = window.innerWidth < 600
		? 1
		: window.innerWidth < 991
			? Math.min(props.items, 2)
			: props.items

	const containerWidth = bglSlider.value.offsetWidth
	const gapWidth = (containerWidth * GAP_PERCENT) / 100
	const totalGapWidth = (itemCount.value - 1) * gapWidth
	const singleItemWidth = (containerWidth - totalGapWidth) / itemCount.value
	const gapsBeforeSlide = activeSlideIndex.value * gapWidth
	const targetTransform = -(singleItemWidth * activeSlideIndex.value + gapsBeforeSlide)

	setTransform(targetTransform)
	updateHeight()
}

function clearAutoplay() {
	if (autoPlayInterval) { clearInterval(autoPlayInterval) }
}

// Mouse events
function preventDefaultClick(e: MouseEvent) {
	if (isDragging.value) {
		e.preventDefault()
		e.stopPropagation()
	}
}

async function disableDrag() {
	await nextTick()
	if (!isSliderAvailable.value || !bglSlider.value) { return }

	const elements = Array.from(bglSlider.value.querySelectorAll('img, a')) as HTMLElement[]
	elements.forEach((el) => {
		el.setAttribute('draggable', 'false')
		// Add click prevention to interactive elements
		el.removeEventListener('click', preventDefaultClick, true)
		el.addEventListener('click', preventDefaultClick, true)
	})

	const slides = Array.from(bglSlider.value.children)
	slides.forEach((slide) => {
		const element = slide as HTMLElement
		element.setAttribute('draggable', 'false')
		// Add click prevention to slides
		element.removeEventListener('click', preventDefaultClick, true)
		element.addEventListener('click', preventDefaultClick, true)
	})
}

function startDrag(e: MouseEvent) {
	e.stopPropagation()
	if (e.button !== 0 || !props.freeDrag || !isSliderAvailable.value) { return }

	clearAutoplay()
	startX.value = e.pageX
	translateX.value = getCurrentTransform()
	isPressed.value = true
	isDragging.value = false // Reset dragging state on start

	document.addEventListener('mousemove', onDrag)
	document.addEventListener('mouseup', endDrag)
}

function onDrag(e: MouseEvent) {
	if (!isSliderAvailable.value || !bglSlider.value || !isPressed.value) { return }

	const x = e.pageX
	const distance = x - startX.value

	if (Math.abs(distance) > THRESHOLDS.DRAG) { isDragging.value = true }
	if (isDragging.value) {
		const newTranslate = translateX.value + (props.rtl ? -distance : distance)
		const maxTranslate = 0

		const containerWidth = bglSlider.value.offsetWidth
		const gapWidth = (containerWidth * GAP_PERCENT) / 100
		const totalGapWidth = (itemCount.value - 1) * gapWidth
		const singleItemWidth = (containerWidth - totalGapWidth) / itemCount.value
		const minTranslate = -(singleItemWidth * (slideCount.value - 1) + gapWidth * (slideCount.value - 1))

		const boundedTranslate = Math.max(minTranslate, Math.min(maxTranslate, newTranslate))
		setTransform(boundedTranslate)
	}
}

function endDrag() {
	isPressed.value = false
	document.removeEventListener('mousemove', onDrag)
	document.removeEventListener('mouseup', endDrag)

	if (!isSliderAvailable.value || !bglSlider.value) { return }

	const currentTransform = getCurrentTransform()
	const singleItemWidth = bglSlider.value.offsetWidth / itemCount.value
	const currentPanel = -currentTransform / singleItemWidth

	const totalDragDistance = currentTransform - translateX.value
	const dragPercentage = Math.abs(totalDragDistance) / singleItemWidth

	let targetPanel = currentPanel
	if (dragPercentage > THRESHOLDS.SWIPE_PERCENT) {
		const distnace = totalDragDistance > 0 ? -1 : 1
		targetPanel = Math.floor(currentPanel) + (distnace < 0 ? 0 : 1)
	} else {
		targetPanel = Math.round(currentPanel)
	}

	targetPanel = Math.max(0, Math.min(targetPanel, slideCount.value - 1))
	goToSlide(targetPanel, true)

	// Keep isDragging true for a short period to prevent click
	if (isDragging.value) {
		setTimeout(() => {
			isDragging.value = false
		}, 10) // Short timeout to ensure click event is handled first
	}
}

// Add a new state variable for tracking scroll direction
const hasScrollDirectionLock = ref(false)
const isHorizontalScroll = ref(false)

function onTouchStart(e: TouchEvent) {
	if (!props.freeDrag || !isSliderAvailable.value) { return }

	const target = e.target as HTMLElement
	const isInteractive = target.matches('button, a, input, select, textarea, [role="button"]')
		|| target.closest('button, a, input, select, textarea, [role="button"]')

	if (isInteractive === false || isInteractive === null) {
		// Don't prevent default immediately - let's wait to see the direction
		clearAutoplay()
		const [touch] = e.touches
		startX.value = touch.pageX
		startY.value = touch.pageY
		lastX.value = touch.pageX
		lastTime.value = Date.now()
		translateX.value = getCurrentTransform()
		velocityTracker.value = [{ time: lastTime.value, position: lastX.value }]
		isPressed.value = true
		hasScrollDirectionLock.value = false

		// Ensure no transition is active when starting touch
		if (bglSlider.value) {
			bglSlider.value.style.transition = 'none'
		}
	}
}

function onTouchMove(e: TouchEvent) {
	if (!isSliderAvailable.value || !bglSlider.value || !isPressed.value) { return }

	const [touch] = e.touches
	const x = touch.pageX
	const y = touch.pageY
	const deltaX = x - startX.value
	const deltaY = y - startY.value

	// Determine scroll direction if we haven't already
	if (!hasScrollDirectionLock.value) {
		// Only lock in a direction if there's significant movement
		if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
			isHorizontalScroll.value = Math.abs(deltaX) > Math.abs(deltaY)
			hasScrollDirectionLock.value = true

			// If it's vertical scrolling, release the touch capture
			if (!isHorizontalScroll.value) {
				isPressed.value = false
				return
			}
		}
		// Wait for more movement before deciding
		return
	}

	// If we've determined this is a vertical scroll, don't handle the touch
	if (!isHorizontalScroll.value) { return }

	// Update velocity tracking
	const now = Date.now()
	velocityTracker.value.push({ time: now, position: x })
	lastX.value = x

	// Keep only recent samples
	const cutoffTime = now - VELOCITY_SAMPLE_DURATION
	velocityTracker.value = velocityTracker.value.filter((sample: VelocitySample) => sample.time >= cutoffTime)

	if (Math.abs(deltaX) > THRESHOLDS.TOUCH) {
		isDragging.value = true
		e.preventDefault() // Only prevent default if we're sure it's a horizontal drag
	}

	if (isDragging.value) {
		const containerWidth = bglSlider.value.offsetWidth
		const gapWidth = (containerWidth * GAP_PERCENT) / 100
		const totalGapWidth = (itemCount.value - 1) * gapWidth
		const singleItemWidth = (containerWidth - totalGapWidth) / itemCount.value
		const totalWidth = (singleItemWidth * slideCount.value) + (gapWidth * (slideCount.value - 1))

		// Calculate the new position based on touch movement
		let newTranslate = translateX.value + (props.rtl ? -deltaX : deltaX)

		// Add smooth resistance when pulling beyond bounds
		if (newTranslate > 0) {
			newTranslate *= 0.5 // Softer resistance at start
		} else if (newTranslate < -totalWidth + containerWidth) {
			const overDrag = -totalWidth + containerWidth - newTranslate
			newTranslate = -totalWidth + containerWidth - (overDrag * 0.5) // Softer resistance at end
		}

		// Apply the transform immediately without any easing
		setTransform(newTranslate)
	}
}

function onTouchEnd() {
	if (!isSliderAvailable.value || !bglSlider.value || !isPressed.value || !isHorizontalScroll.value) {
		isPressed.value = false
		hasScrollDirectionLock.value = false
		return
	}

	const containerWidth = bglSlider.value.offsetWidth
	const gapWidth = (containerWidth * GAP_PERCENT) / 100
	const totalGapWidth = (itemCount.value - 1) * gapWidth
	const singleItemWidth = (containerWidth - totalGapWidth) / itemCount.value
	const totalSlideWidth = singleItemWidth + gapWidth

	const currentTransform = getCurrentTransform()

	// Normalize the transform value to handle overscroll
	let normalizedTransform = currentTransform
	if (currentTransform > 0) {
		normalizedTransform = 0
	} else {
		const minTransform = -(singleItemWidth * (slideCount.value - 1) + gapWidth * (slideCount.value - 1))
		if (currentTransform < minTransform) {
			normalizedTransform = minTransform
		}
	}

	const currentPanel = Math.abs(normalizedTransform / totalSlideWidth)

	const totalSwipeDistance = lastX.value - startX.value
	const swipePercentage = Math.abs(totalSwipeDistance) / totalSlideWidth

	// Get final velocity from tracker
	const finalVelocity = getAverageVelocity()

	let targetPanel = currentPanel
	let velocityDirection = 0

	// Determine direction based on velocity and distance
	if (Math.abs(finalVelocity) > THRESHOLDS.VELOCITY) {
		velocityDirection = finalVelocity < 0 ? 1 : -1
	}

	const isMobile = window.innerWidth < 600

	if (swipePercentage > THRESHOLDS.SWIPE_PERCENT || Math.abs(finalVelocity) > THRESHOLDS.VELOCITY) {
		const direction = swipePercentage > THRESHOLDS.SWIPE_PERCENT
			? (totalSwipeDistance > 0 ? -1 : 1)
			: velocityDirection

		// Apply RTL correction to direction
		const rtlDirection = props.rtl ? -direction : direction

		if (isMobile) {
			// On mobile, only move one slide at a time
			targetPanel = activeSlideIndex.value + rtlDirection
		} else {
			// On desktop, keep the current behavior
			targetPanel = Math.round(currentPanel) + rtlDirection
		}
	} else {
		// If the swipe wasn't strong enough, snap back to the current slide
		targetPanel = isMobile ? activeSlideIndex.value : Math.round(currentPanel)
	}

	targetPanel = Math.max(0, Math.min(targetPanel, slideCount.value - 1))
	activeSlideIndex.value = targetPanel

	// Adjust animation duration based on velocity and distance to travel
	const distance = Math.abs(targetPanel - currentPanel)
	const velocityFactor = Math.min(Math.abs(finalVelocity) * 1000, 1)
	const duration = Math.min(
		ANIMATION_TIMINGS.BASE_DURATION * (1 - velocityFactor * 0.5) + (distance * ANIMATION_TIMINGS.DURATION_PER_PANEL),
		ANIMATION_TIMINGS.MAX_DURATION
	)

	// Re-enable transitions for the snap animation
	bglSlider.value.style.transition = `transform ${duration}ms cubic-bezier(0.4, 0, 0.2, 1)`

	const gapsBeforeSlide = targetPanel * gapWidth
	const targetTransform = -(singleItemWidth * targetPanel + gapsBeforeSlide)
	setTransform(targetTransform)

	// Clear transition after animation
	setTimeout(() => {
		if (bglSlider.value) { bglSlider.value.style.transition = 'none' }
	}, duration)

	velocityTracker.value = []
	isDragging.value = false
	hasScrollDirectionLock.value = false
}

// Wheel events
function onWheel(e: WheelEvent) {
	if (!props.allowScroll || !isSliderAvailable.value || !bglSlider.value || isPressed.value || isDragging.value) { return }

	const isHorizontal = Math.abs(e.deltaX) > Math.abs(e.deltaY)
	if (!isHorizontal) { return }

	clearAutoplay()
	if (wheelTimeout) { clearTimeout(wheelTimeout) }

	// Reverse the delta direction for RTL mode
	accumulatedDeltaX.value += props.rtl ? -e.deltaX : e.deltaX

	wheelTimeout = setTimeout(() => {
		accumulatedDeltaX.value = 0
	}, 50)

	const singleItemWidth = bglSlider.value.offsetWidth / itemCount.value
	const moveThreshold = singleItemWidth * THRESHOLDS.WHEEL_PERCENT

	if (Math.abs(accumulatedDeltaX.value) > moveThreshold) {
		const direction = accumulatedDeltaX.value > 0 ? 1 : -1
		const nextPanel = Math.max(0, Math.min(activeSlideIndex.value + direction, slideCount.value - 1))

		if (nextPanel !== activeSlideIndex.value) {
			goToSlide(nextPanel, true)
			accumulatedDeltaX.value = 0
		}
	}
}

// Lifecycle
onMounted(() => {
	window.addEventListener('resize', handleResize)
	updateHeight()
	handleResize()
	countSlides()
	disableDrag()

	if (bglSlider.value) {
		slideCount.value = bglSlider.value.children.length
	}

	if (props.autoplay) {
		autoPlayInterval = setInterval(next, props.autoPlaySpeed)
	}
})

onUnmounted(() => {
	if (isSliderAvailable.value && bglSlider.value) {
		Array.from(bglSlider.value.children).forEach((child) => {
			const element = child as HTMLElement
			element.removeEventListener('click', preventDefaultClick)
		})
	}
	window.removeEventListener('resize', handleResize)
	document.removeEventListener('mousemove', onDrag)
	document.removeEventListener('mouseup', endDrag)
	clearAutoplay()
	if (timeout) { clearTimeout(timeout) }
	if (wheelTimeout) { clearTimeout(wheelTimeout) }
})

// Watchers
watch(() => props.index, (newIndex: number) => { goToSlide(newIndex) })
watch(() => activeSlideIndex.value, handleSlideChange)

defineExpose({
	goToSlide,
	next,
	prev,
	countSlides,
	clearAutoplay,
})
</script>

<template>
	<div class="BglCarousel" :dir="rtl ? 'rtl' : 'ltr'">
		<div
			ref="bglSlider" class="bgl-slider"
			:class="{ dragging: isDragging, clicking: isPressed, [`slides-${itemCount}`]: true, grab: freeDrag && slideCount > 1, autoHeight }"
			:style="{ '--item-count': itemCount, 'height': yHeight }" @mousedown="startDrag" @mouseover="clearAutoplay"
			@focusin="clearAutoplay" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd"
			@wheel="onWheel"
		>
			<slot />
		</div>
		<div v-if="props.dots && slideCount > 1" class="dots">
			<span
				v-for="i in slideCount" :key="i" class="dot" :class="{ current: activeSlideIndex === i - 1 }"
				@click="goToSlide(i - 1)"
			/>
		</div>
		<div class="navigation-buttons" :class="{ rtl }">
			<span class="prev" @click="prev">
				<slot name="prev" :index="activeSlideIndex" :prev="prev" />
			</span>
			<span class="next" @click="next">
				<slot name="next" :index="activeSlideIndex" :next="next" />
			</span>
		</div>
	</div>
</template>

<style scoped>
.BglCarousel {
	position: relative;
	width: 100%;
	overflow: hidden;
	touch-action: pan-y pinch-zoom;
	/* Allow vertical scrolling */
}

.bgl-slider {
	display: flex;
	position: relative;
	width: 100%;
	touch-action: pan-y pinch-zoom;
	/* Allow vertical scrolling */
	will-change: transform;
	transform: translateX(0);
	gap: 1%;
	transition: none;
	-webkit-user-select: none;
	user-select: none;
}

.bgl-slider>* {
	flex: 0 0 calc((100% - (var(--item-count) - 1) * 1%) / var(--item-count));
	width: calc((100% - (var(--item-count) - 1) * 1%) / var(--item-count));
	min-width: calc((100% - (var(--item-count) - 1) * 1%) / var(--item-count));
	position: relative;
	overflow: hidden;
}

.bgl-slider.slides-1>* {
	flex: 0 0 100%;
	width: 100%;
	min-width: 100%;
}

.dragging .bgl-slider>* {
	pointer-events: none;
	user-select: none;
}

.autoHeight {
	transition: height ease 0.7s;
}

.dots {
	display: flex;
	justify-content: center;
	align-items: center;
	width: 100%;
	margin-top: 1rem;
	gap: 8px;
}

.dot {
	height: 10px;
	width: 10px;
	border-radius: 50%;
	background-color: var(--bgl-black);
	opacity: 0.4;
	transition: opacity 0.3s ease;
	cursor: pointer;
}

.dot:hover {
	opacity: 0.6;
}

.dot.current {
	opacity: 0.8;
}

.navigation-buttons {
	display: flex;
	justify-content: center;
	align-items: center;
	gap: 1rem;
	margin-top: 1rem;
}

.navigation-buttons.rtl {
	flex-direction: row-reverse;
}
</style>
