<template>
	<view
		v-if="visible"
		class="vv-root"
		:class="{ 'vv-root--closing': animPhase === 'exiting' }"
		:style="{ ...rootStyle, ...animationStyles.root }"
		@click="animPhase !== 'exiting' && onOverlayClick()"
	>
		<view
			class="vv-backdrop"
			:class="{ 'vv-backdrop--expanded': expanded, 'vv-backdrop--closing': animPhase === 'exiting' }"
			:style="animationStyles.backdrop"
			@click.stop
		/>
		<view
			class="vv-transform-wrap"
			:style="{ ...transformWrapStyle, ...animationStyles.transformWrap }"
			@click.stop="animPhase !== 'exiting' && requestClose()"
		>
			<!-- ✨ 结构稳如磐石，不再依据 expanded 动态销毁，彻底根除闪屏 -->
			<view
				v-if="showSingleContent"
				class="vv-pinch-wrap"
				:style="pinchWrapStyle"
			>
				<!-- 封面图坚挺防线，直到视频放出第一帧才隐退 -->
				<image
					v-if="currentItem?.thumb_url && !isVideoStarted"
					class="vv-thumb"
					:src="currentItem.thumb_url"
					mode="aspectFit"
				/>
				<!-- 视频常驻 DOM，但用隐藏类名代替销毁 -->
				<video
					:id="getVideoId(currentIndex)"
					class="vv-video"
					:class="{ 'vv-video--hide': animPhase !== 'displaying' }"
					:src="currentItem?.media_url"
					object-fit="contain"
					:autoplay="false"
					playsinline="true"
					webkit-playsinline="true"
					x5-playsinline="true"
					x5-video-player-type="h5"
					x5-video-player-fullscreen="false"
					:controls="false"
					:show-center-play-btn="false"
					:show-fullscreen-btn="false"
					:show-play-btn="false"
					:show-progress="false"
					:enable-progress-gesture="false"
					:loop="false"
					:muted="false"
					@play="onVideoPlay"
					@pause="onVideoPause"
					@ended="onVideoEnded"
					@waiting="onVideoWaiting"
					@error="onVideoError"
				/>
			</view>
			
			<view v-if="showSwiper" class="vv-slides-view">
				<view class="vv-slides-track" :style="slidesTrackStyle">
					<view
						v-for="(item, i) in items"
						:key="i"
						class="vv-slide"
						:style="{ width: slideWidthPx + 'px' }"
						v-show="isSlideVisible(i)"
						@touchstart="onSlideTouchStart"
						@touchmove="onSlideTouchMove"
						@touchend="onSlideTouchEnd"
						@touchcancel="onSlideTouchEnd"
					>
						<view class="vv-pinch-wrap">
							<image
								v-if="item?.thumb_url && (i !== currentIndex || !isVideoStarted)"
								class="vv-thumb"
								:src="item.thumb_url"
								mode="aspectFit"
							/>
							<video
								v-if="i === currentIndex"
								:id="getVideoId(i)"
								class="vv-video"
								:class="{ 'vv-video--hide': animPhase !== 'displaying' }"
								:src="item.media_url"
								object-fit="contain"
								:autoplay="false"
								playsinline="true"
								webkit-playsinline="true"
								x5-playsinline="true"
								x5-video-player-type="h5"
								x5-video-player-fullscreen="false"
								:controls="false"
								:show-center-play-btn="false"
								:show-fullscreen-btn="false"
								:show-play-btn="false"
								:show-progress="false"
								:enable-progress-gesture="false"
								:loop="false"
								@play="onVideoPlay"
								@pause="onVideoPause"
								@ended="onVideoEnded"
								@waiting="onVideoWaiting"
								@error="onVideoError"
							/>
						</view>
					</view>
				</view>
			</view>
			<view v-show="showLoading" class="vv-loading">
				<view class="vv-loading-spinner"></view>
			</view>
		</view>
		<view v-show="showSwiper" class="vv-dots-wrap" @click.stop="requestClose">
			<view class="vv-dots">
				<text class="vv-dots__text">{{ currentIndex + 1 }} / {{ items.length }}</text>
			</view>
		</view>
	</view>
</template>

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

export type InitialRect = { left: number; top: number; width: number; height: number }
export type VideoItem = { media_url: string; thumb_url?: string }

const SLIDE_DRAG_THRESHOLD = 60
const SLIDE_ANIMATION_MS = 200
const SLIDE_EASE = 'cubic-bezier(0.16, 1, 0.3, 1)'
const SLIDE_DRAG_FACTOR = 1.35
const OPEN_BLUR_S = 0.25
const OPEN_ANIMATION_EASE = 'cubic-bezier(0.25, 0.46, 0.45, 0.94)'
const EXIT_BLUR_S = 0.25

const props = withDefaults(
	defineProps<{
		visible: boolean
		items: VideoItem[]
		current?: number
		initialRect?: InitialRect | null
		zIndex?: number
	}>(),
	{ current: 0, initialRect: null, zIndex: 9999 }
)

const emit = defineEmits<{ close: [] }>()

const tabRootBackPress = inject<{
	register: (h: { check: () => boolean; close: () => void }) => () => void
}>('tabRootBackPress', undefined as any)

type AnimPhase = 'entering' | 'displaying' | 'exiting'
const animPhase = ref<AnimPhase | null>(null)
const enterPaintReady = ref(false)
const enterTargetPainted = ref(false)
let enterCompleteTimer: ReturnType<typeof setTimeout> | null = null
let exitCompleteTimer: ReturnType<typeof setTimeout> | null = null

const expanded = ref(false)
const currentIndex = ref(0)
const windowSize = ref({ width: 375, height: 667 })
const isVideoPlaying = ref(false)
const isVideoLoading = ref(false)
const isVideoStarted = ref(false)

const videoContextRef = shallowRef<UniApp.VideoContext | null>(null)

let unregisterBack: (() => void) | null = null
let videoContextMap = new Map<number, UniApp.VideoContext>()

const uid = Date.now()

function getVideoId(index: number): string {
	return `vv-video-${uid}-${index}`
}

function getVideoContext(index: number): UniApp.VideoContext | null {
	if (!videoContextMap.has(index)) {
		const id = getVideoId(index)
		videoContextMap.set(index, uni.createVideoContext(id))
	}
	return videoContextMap.get(index) || null
}

const showLoading = computed(() => isVideoLoading.value && !isVideoPlaying.value)

function clearAnimTimers() {
	if (enterCompleteTimer) {
		clearTimeout(enterCompleteTimer)
		enterCompleteTimer = null
	}
	if (exitCompleteTimer) {
		clearTimeout(exitCompleteTimer)
		exitCompleteTimer = null
	}
}

function nextFrame(fn: () => void) {
	if (typeof requestAnimationFrame !== 'undefined') requestAnimationFrame(fn)
	else setTimeout(fn, 0)
}

function startEnterPhase() {
	animPhase.value = 'entering'
	enterPaintReady.value = false
	enterTargetPainted.value = false
	clearAnimTimers()
	nextFrame(() => {
		enterPaintReady.value = true
		nextFrame(() => { enterTargetPainted.value = true })
	})
	enterCompleteTimer = setTimeout(() => {
		enterCompleteTimer = null
		animPhase.value = 'displaying'
		// ✨ 动画结束，节点归位，由代码安全唤起播放
		playCurrentVideo()
	}, OPEN_BLUR_S * 1000)
}

function startExitPhase(onComplete: () => void) {
	animPhase.value = 'exiting'
	clearAnimTimers()
	exitCompleteTimer = setTimeout(() => {
		exitCompleteTimer = null
		onComplete()
	}, EXIT_BLUR_S * 1000)
}

function playCurrentVideo() {
	nextTick(() => {
		const ctx = getVideoContext(currentIndex.value)
		if (ctx) {
			ctx.play()
		}
	})
}

function stopAllVideo() {
	videoContextMap.forEach(ctx => {
		if (ctx) {
			try { ctx.pause() } catch {}
		}
	})
}

function onVideoPlay() {
	isVideoPlaying.value = true
	isVideoLoading.value = false
	isVideoStarted.value = true
}

function onVideoPause() {
	isVideoPlaying.value = false
}

function onVideoEnded() {
	isVideoPlaying.value = false
}

function onVideoWaiting() {
	isVideoLoading.value = true
}

function onVideoError(e: any) {
	console.error('[VideoViewer] 视频播放错误:', e)
	isVideoLoading.value = false
	isVideoPlaying.value = false
}

const dragOffsetPx = ref(0)
const slideTransitionOn = ref(false)
let panStart: { startX: number; baseDragOffset: number } | null = null

const slideWidthPx = computed(() => windowSize.value.width)
const slidesTrackStyle = computed(() => {
	const w = windowSize.value.width
	const n = props.items?.length ?? 0
	const tx = n <= 0 ? 0 : -currentIndex.value * w + dragOffsetPx.value
	return {
		width: `${n * w}px`,
		transform: `translateX(${tx}px)`,
		transition: slideTransitionOn.value ? `transform ${SLIDE_ANIMATION_MS}ms ${SLIDE_EASE}` : 'none'
	}
})

function clamp(v: number, min: number, max: number) {
	return Math.max(min, Math.min(max, v))
}

function isSlideVisible(i: number) {
	const c = currentIndex.value
	return i >= c - 1 && i <= c + 1
}

const pinchWrapStyle = computed(() => ({ transform: 'translate(0, 0) scale(1)' }))

function handleSlideDrag(t: { clientX: number }) {
	if (!panStart) return
	const dx = t.clientX - panStart.startX
	const w = windowSize.value.width
	dragOffsetPx.value = clamp((panStart.baseDragOffset ?? 0) + dx * SLIDE_DRAG_FACTOR, -w, w)
}

function goToSlide(delta: -1 | 1) {
	const n = props.items?.length || 0
	if (n <= 1) return
	const oldIdx = currentIndex.value
	const nextIdx = delta < 0 ? Math.max(0, oldIdx - 1) : Math.min(n - 1, oldIdx + 1)
	if (nextIdx === oldIdx) return
	
	stopAllVideo()
	isVideoPlaying.value = false
	isVideoStarted.value = false 
	
	currentIndex.value = nextIdx
	slideTransitionOn.value = true
	dragOffsetPx.value = 0
	
	setTimeout(() => { 
		slideTransitionOn.value = false 
		if (animPhase.value === 'displaying') {
			playCurrentVideo()
		}
	}, SLIDE_ANIMATION_MS)
}

function onSlideTouchStart(e: any) {
	if (e.touches.length === 1) {
		panStart = { startX: e.touches[0].clientX, baseDragOffset: dragOffsetPx.value }
	}
}

function onSlideTouchMove(e: any) {
	if (e.touches.length === 1 && panStart) {
		e.preventDefault()
		handleSlideDrag(e.touches[0])
	}
}

function onSlideTouchEnd(e: any) {
	if (e.touches.length === 0) {
		const n = props.items?.length ?? 0
		if (n <= 1) { panStart = null; return }
		const th = SLIDE_DRAG_THRESHOLD
		if (dragOffsetPx.value > th && currentIndex.value > 0) goToSlide(-1)
		else if (dragOffsetPx.value < -th && currentIndex.value < n - 1) goToSlide(1)
		else {
			slideTransitionOn.value = true
			dragOffsetPx.value = 0
			setTimeout(() => { slideTransitionOn.value = false }, SLIDE_ANIMATION_MS)
		}
		panStart = null
	} else if (e.touches.length === 1 && panStart) {
		panStart = { ...panStart, baseDragOffset: dragOffsetPx.value }
	}
}

const isMulti = computed(() => (props.items?.length || 0) > 1)

const currentItem = computed(() => {
	const list = props.items || []
	const i = Math.max(0, Math.min(currentIndex.value, list.length - 1))
	return list[i] || null
})

// ✨ 单轨与多轨完全基于数据长度固定，生命周期内再无结构突变，杜绝 OS 重排闪屏
const showSwiper = computed(() => isMulti.value)
const showSingleContent = computed(() => !isMulti.value)

const rootStyle = computed(() => ({ zIndex: props.zIndex }))

const animationStyles = computed(() => {
	const phase = animPhase.value
	const transOpen = `opacity ${OPEN_BLUR_S}s ${OPEN_ANIMATION_EASE}`
	const transExit = `opacity ${EXIT_BLUR_S}s ease-out`
	
	if (phase === 'exiting') {
		return {
			root: { transition: transExit, opacity: 0, pointerEvents: 'none' as const },
			backdrop: { transition: `opacity ${EXIT_BLUR_S}s ease-out`, opacity: 0 },
			transformWrap: { transition: transExit, opacity: 0, pointerEvents: 'none' as const }
		}
	}
	
	if (phase === 'entering' || phase === 'displaying') {
		const applyTarget = phase === 'displaying' || (phase === 'entering' && enterPaintReady.value)
		if (!applyTarget) {
			return { root: { opacity: 0 }, backdrop: { opacity: 0 }, transformWrap: { opacity: 0 } }
		}
		return {
			root: { transition: transOpen, opacity: 1 },
			backdrop: { transition: transOpen, opacity: 1 },
			transformWrap: { transition: transOpen, opacity: 1 }
		}
	}
	return { root: {}, backdrop: {}, transformWrap: {} }
})

const transformWrapStyle = computed(() => {
	return {
		left: '0',
		top: '0',
		right: '0',
		bottom: '0',
		width: '100%',
		height: '100%',
		opacity: 0
	}
})

function registerBack() {
	if (tabRootBackPress) {
		unregisterBack = tabRootBackPress.register({
			check: () => props.visible,
			close
		})
	}
}

function unregisterBackHandler() {
	if (unregisterBack) {
		unregisterBack()
		unregisterBack = null
	}
}

function onOverlayClick() {
	requestClose()
}

function requestClose() {
	if (animPhase.value === 'exiting') return
	stopAllVideo()
	isVideoPlaying.value = false
	isVideoLoading.value = false
	isVideoStarted.value = false 
	nextFrame(() => {
		startExitPhase(() => {
			expanded.value = false
			animPhase.value = null
			emit('close')
		})
	})
}

function close() {
	requestClose()
}

watch(
	() => props.visible,
	(v) => {
		if (v) {
			try {
				const sys = uni.getSystemInfoSync()
				windowSize.value = {
					width: sys.windowWidth ?? sys.screenWidth ?? 375,
					height: sys.windowHeight ?? sys.screenHeight ?? 667
				}
			} catch {
				windowSize.value = { width: 375, height: 667 }
			}
			currentIndex.value = Math.max(0, Math.min(props.current, (props.items?.length || 1) - 1))
			isVideoPlaying.value = false
			isVideoLoading.value = false
			isVideoStarted.value = false
			videoContextMap.clear()
			expanded.value = !props.initialRect || props.initialRect.width <= 0 || props.initialRect.height <= 0
			registerBack()
			if (props.initialRect && props.initialRect.width > 0 && props.initialRect.height > 0) {
				setTimeout(() => {
					expanded.value = true
					startEnterPhase()
				}, 50)
			} else {
				expanded.value = true
				startEnterPhase()
			}
		} else {
			expanded.value = false
			animPhase.value = null
			enterPaintReady.value = false
			enterTargetPainted.value = false
			isVideoPlaying.value = false
			isVideoLoading.value = false
			isVideoStarted.value = false
			clearAnimTimers()
			dragOffsetPx.value = 0
			slideTransitionOn.value = false
			panStart = null
			stopAllVideo()
			videoContextMap.clear()
			unregisterBackHandler()
		}
	},
	{ immediate: true }
)

watch(() => props.current, (v) => {
	currentIndex.value = Math.max(0, Math.min(v, (props.items?.length || 1) - 1))
})

onUnmounted(() => {
	clearAnimTimers()
	unregisterBackHandler()
	stopAllVideo()
	videoContextMap.clear()
})
</script>

<style scoped>
.vv-root {
	position: fixed;
	left: 0;
	top: 0;
	right: 0;
	bottom: 0;
	width: 100%;
	height: 100%;
	display: flex;
	align-items: center;
	justify-content: center;
	overflow: hidden;
}

.vv-root--closing {
	opacity: 0;
}

.vv-backdrop {
	position: absolute;
	left: 0;
	top: 0;
	right: 0;
	bottom: 0;
	background: rgba(0, 0, 0, 0.4);
	opacity: 0;
	transition: opacity 0.3s ease;
}

.vv-backdrop--expanded {
	background: rgba(0, 0, 0, 0.94);
	opacity: 1;
}

.vv-backdrop--closing {
	opacity: 0;
}

.vv-transform-wrap {
	position: fixed;
	display: flex;
	align-items: center;
	justify-content: center;
	overflow: hidden;
	z-index: 1;
}

.vv-pinch-wrap {
	width: 100%;
	height: 100%;
	position: relative;
	display: block; 
	touch-action: none;
}

.vv-video {
	position: absolute;
	left: 0;
	top: 0;
	width: 100%;
	height: 100%;
	display: block;
	z-index: 1;
}

/* ✨ 修复：进入和退出期间，利用极端的缩放和位移将 video 丢出屏幕 */
.vv-video--hide {
	transform: translateX(-9999px) scale(0.001);
	opacity: 0;
	pointer-events: none;
}

.vv-thumb {
	position: absolute;
	left: 0;
	top: 0;
	width: 100%;
	height: 100%;
	display: block;
	z-index: 2; 
}

.vv-slides-view {
	position: absolute;
	left: 0;
	top: 0;
	right: 0;
	bottom: 0;
	width: 100%;
	height: 100%;
	overflow: hidden;
}

.vv-slides-track {
	display: flex;
	height: 100%;
	flex-shrink: 0;
	will-change: transform;
}

.vv-slide {
	flex: 0 0 auto;
	height: 100%;
	display: flex;
	align-items: center;
	justify-content: center;
	overflow: hidden;
}

.vv-dots-wrap {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	z-index: 2;
	display: flex;
	justify-content: center;
	align-items: flex-end;
	padding-bottom: calc(60rpx + env(safe-area-inset-bottom));
	pointer-events: none;
}

.vv-dots {
	display: flex;
	justify-content: center;
}

.vv-dots__text {
	font-size: 28rpx;
	color: rgba(255, 255, 255, 0.9);
	background: rgba(0, 0, 0, 0.4);
	padding: 8rpx 24rpx;
	border-radius: 24rpx;
}

.vv-loading {
	position: absolute;
	left: 50%;
	top: 50%;
	transform: translate(-50%, -50%);
	z-index: 10;
	pointer-events: none;
}

.vv-loading-spinner {
	width: 60rpx;
	height: 60rpx;
	border: 4rpx solid rgba(255, 255, 255, 0.3);
	border-top-color: #fff;
	border-radius: 50%;
	animation: vv-spin 0.8s linear infinite;
}

@keyframes vv-spin {
	to { transform: rotate(360deg); }
}
</style>