<template>
    <div v-if="visible" class="mobile-image-preview-mask" role="dialog" aria-modal="true" :style="maskStyle"
        @click="handleClose">
        <button class="mobile-image-preview-close" type="button" :aria-label="t('关闭图片预览')" @click.stop="handleClose">
            ×
        </button>

        <button class="mobile-image-preview-download" type="button" :aria-label="t('下载图片')"
            @click.stop="handleDownload">
            <img :src="iconDownload" alt="download" />
        </button>

        <div ref="trackRef" class="mobile-image-preview-track" :style="trackStyle" @click.stop
            @scroll.passive="handleScroll" @touchstart="handleTouchStart" @touchmove="handleTouchMove"
            @touchend="handleTouchEnd" @touchcancel="handleTouchCancel">
            <div v-for="(image, index) in images" :key="image.id" class="mobile-image-preview-slide">
                <img :ref="(element) => setImageRef(element, index)" class="mobile-image-preview-image" :src="image.url"
                    draggable="false" :style="getImageStyle(index)" @click.stop="handleImageClick" />
            </div>
        </div>

        <div v-if="images.length > 1" class="mobile-image-preview-indicator" @click.stop>
            {{ safeActiveIndex + 1 }}/{{ images.length }}
        </div>
    </div>
</template>

<script setup lang="ts">
import { computed, nextTick, onUnmounted, ref, watch, type ComponentPublicInstance, type CSSProperties } from 'vue'
import { useI18n } from 'vue-i18n'

import iconDownload from '../../../assets/icons/download-white.svg'

interface PreviewImageItem {
    id: string
    url: string
}

interface TouchState {
    startX: number
    startY: number
    startScrollLeft: number
    startIndex: number
    startOffsetX: number
    startOffsetY: number
}

const props = defineProps<{
    visible: boolean
    images: PreviewImageItem[]
    activeIndex: number
}>()

const emit = defineEmits<{
    close: []
    'update:activeIndex': [index: number]
}>()

const { t } = useI18n()

const SWIPE_CHANGE_THRESHOLD = 72

const trackRef = ref<HTMLDivElement | null>(null)
const imageRefs = ref<(HTMLImageElement | null)[]>([])
const scale = ref(1)
const imageOffsetX = ref(0)
const imageOffsetY = ref(0)
const offsetY = ref(0)
const transitionEnabled = ref(false)
const dismissDragging = ref(false)
const lastTapAt = ref(0)
const isPointerDown = ref(false)
const isSyncingScroll = ref(false)
const scrollEndTimer = ref<number | null>(null)
const horizontalDragging = ref(false)
const swipeDeltaX = ref(0)
const touchState = ref<TouchState>({
    startX: 0,
    startY: 0,
    startScrollLeft: 0,
    startIndex: 0,
    startOffsetX: 0,
    startOffsetY: 0,
})

const safeActiveIndex = computed(() => {
    if (props.images.length === 0) {
        return 0
    }

    return Math.max(0, Math.min(props.activeIndex, props.images.length - 1))
})

const setImageRef = (element: Element | ComponentPublicInstance | null, index: number) => {
    imageRefs.value[index] = element instanceof HTMLImageElement ? element : null
}

const clamp = (value: number, min: number, max: number) => {
    return Math.min(max, Math.max(min, value))
}

const getActiveImagePanLimits = () => {
    const imageElement = imageRefs.value[safeActiveIndex.value]
    const slideElement = imageElement?.parentElement instanceof HTMLElement
        ? imageElement.parentElement
        : null

    if (!imageElement || !slideElement || typeof window === 'undefined') {
        return {
            maxOffsetX: 0,
            maxOffsetY: 0,
        }
    }

    const slideStyle = window.getComputedStyle(slideElement)
    const paddingLeft = Number.parseFloat(slideStyle.paddingLeft) || 0
    const paddingRight = Number.parseFloat(slideStyle.paddingRight) || 0
    const paddingTop = Number.parseFloat(slideStyle.paddingTop) || 0
    const paddingBottom = Number.parseFloat(slideStyle.paddingBottom) || 0
    const visibleWidth = Math.max(0, slideElement.clientWidth - paddingLeft - paddingRight)
    const visibleHeight = Math.max(0, slideElement.clientHeight - paddingTop - paddingBottom)
    const scaledWidth = imageElement.clientWidth * scale.value
    const scaledHeight = imageElement.clientHeight * scale.value

    return {
        maxOffsetX: Math.max(0, (scaledWidth - visibleWidth) / 2),
        maxOffsetY: Math.max(0, (scaledHeight - visibleHeight) / 2),
    }
}

const clampImageOffsets = (nextOffsetX: number, nextOffsetY: number) => {
    const { maxOffsetX, maxOffsetY } = getActiveImagePanLimits()

    return {
        offsetX: clamp(nextOffsetX, -maxOffsetX, maxOffsetX),
        offsetY: clamp(nextOffsetY, -maxOffsetY, maxOffsetY),
    }
}

const maskStyle = computed<CSSProperties>(() => {
    const opacity = Math.max(0.56, 0.96 - Math.min(Math.abs(offsetY.value) / 320, 0.4))

    return {
        background: `rgba(0, 0, 0, ${opacity})`,
        transition: transitionEnabled.value ? 'background 0.22s ease' : 'none',
    }
})

const trackStyle = computed<CSSProperties>(() => {
    return {
        transform: `translate3d(0, ${offsetY.value}px, 0)`,
        transition: transitionEnabled.value ? 'transform 0.22s ease' : 'none',
        overflowX: dismissDragging.value ? 'hidden' : 'auto',
        scrollSnapType: dismissDragging.value || horizontalDragging.value ? 'none' : 'x mandatory',
    }
})

const resetInteraction = () => {
    scale.value = 1
    imageOffsetX.value = 0
    imageOffsetY.value = 0
    offsetY.value = 0
    transitionEnabled.value = false
    dismissDragging.value = false
    horizontalDragging.value = false
    swipeDeltaX.value = 0
    lastTapAt.value = 0
    touchState.value = {
        startX: 0,
        startY: 0,
        startScrollLeft: 0,
        startIndex: 0,
        startOffsetX: 0,
        startOffsetY: 0,
    }
}

const finishTransition = () => {
    window.setTimeout(() => {
        transitionEnabled.value = false
    }, 220)
}

const clearScrollEndTimer = () => {
    if (scrollEndTimer.value === null) {
        return
    }

    window.clearTimeout(scrollEndTimer.value)
    scrollEndTimer.value = null
}

const restorePosition = () => {
    transitionEnabled.value = true
    offsetY.value = 0
    dismissDragging.value = false
    finishTransition()
}

const lockBodyScroll = (locked: boolean) => {
    if (typeof document === 'undefined') {
        return
    }

    document.body.style.overflow = locked ? 'hidden' : ''
}

const scrollToIndex = (index: number, behavior: ScrollBehavior = 'auto') => {
    const container = trackRef.value
    if (!container || props.images.length === 0) {
        return
    }

    const safeIndex = Math.max(0, Math.min(index, props.images.length - 1))
    isSyncingScroll.value = true
    container.scrollTo({
        left: safeIndex * container.clientWidth,
        behavior,
    })

    window.setTimeout(() => {
        isSyncingScroll.value = false
    }, behavior === 'smooth' ? 260 : 80)
}

const handleClose = () => {
    emit('close')
}

const getCurrentImage = () => {
    return props.images[safeActiveIndex.value]
}

const getDownloadFileName = (url: string) => {
    try {
        const pathname = new URL(url, window.location.href).pathname
        const fileName = pathname.split('/').pop()
        return fileName || 'image'
    } catch {
        return 'image'
    }
}

const handleDownload = async () => {
    const currentImage = getCurrentImage()
    if (!currentImage?.url) {
        return
    }

    const fileName = getDownloadFileName(currentImage.url)

    try {
        const response = await fetch(currentImage.url, {
            mode: 'cors',
            headers: new Headers({
                'Content-Type': 'application/x-www-form-urlencoded',
            }),
        })
        const blob = await response.blob()
        const anchor = document.createElement('a')
        const url = window.URL.createObjectURL(blob)
        anchor.href = url
        anchor.download = fileName
        anchor.click()
        window.URL.revokeObjectURL(url)
    } catch {
        const anchor = document.createElement('a')
        anchor.href = currentImage.url
        anchor.target = '_blank'
        anchor.download = fileName
        anchor.click()
    }
}

const getSwipeTargetIndex = () => {
    const container = trackRef.value
    if (!container || props.images.length === 0) {
        return safeActiveIndex.value
    }

    const threshold = Math.max(SWIPE_CHANGE_THRESHOLD, container.clientWidth * 0.18)
    let nextIndex = touchState.value.startIndex

    if (Math.abs(swipeDeltaX.value) >= threshold) {
        nextIndex += swipeDeltaX.value < 0 ? 1 : -1
    }

    return Math.max(0, Math.min(nextIndex, props.images.length - 1))
}

const handleScroll = () => {
    const container = trackRef.value
    if (!container || props.images.length === 0) {
        return
    }

    const width = container.clientWidth || 1
    const nextIndex = Math.round(container.scrollLeft / width)
    const safeIndex = Math.max(0, Math.min(nextIndex, props.images.length - 1))

    if (safeIndex !== safeActiveIndex.value) {
        scale.value = 1
        imageOffsetX.value = 0
        imageOffsetY.value = 0
    }

    if (isPointerDown.value || isSyncingScroll.value) {
        return
    }

    emit('update:activeIndex', safeIndex)

    clearScrollEndTimer()
    scrollEndTimer.value = window.setTimeout(() => {
        if (!isPointerDown.value && !dismissDragging.value) {
            scrollToIndex(safeIndex, 'smooth')
        }
    }, 80)
}

const getImageStyle = (index: number): CSSProperties => {
    return {
        transform: index === safeActiveIndex.value
            ? `translate3d(${imageOffsetX.value}px, ${imageOffsetY.value}px, 0) scale(${scale.value})`
            : 'translate3d(0, 0, 0) scale(1)',
        transition: transitionEnabled.value ? 'transform 0.22s ease' : 'none',
    }
}

const toggleZoom = () => {
    transitionEnabled.value = true
    scale.value = scale.value > 1 ? 1 : 2
    imageOffsetX.value = 0
    imageOffsetY.value = 0
    offsetY.value = 0
    dismissDragging.value = false
    finishTransition()
}

const handleImageClick = () => {
    const now = Date.now()

    if (now - lastTapAt.value < 260) {
        lastTapAt.value = 0
        toggleZoom()
        return
    }

    lastTapAt.value = now
}

const handleTouchStart = (event: TouchEvent) => {
    if (event.touches.length !== 1) {
        return
    }

    const touch = event.touches[0]
    if (!touch) {
        return
    }

    const container = trackRef.value
    touchState.value = {
        startX: touch.clientX,
        startY: touch.clientY,
        startScrollLeft: container?.scrollLeft ?? 0,
        startIndex: safeActiveIndex.value,
        startOffsetX: imageOffsetX.value,
        startOffsetY: imageOffsetY.value,
    }
    isPointerDown.value = true
    dismissDragging.value = false
    horizontalDragging.value = false
    swipeDeltaX.value = 0
    transitionEnabled.value = false
    clearScrollEndTimer()
}

const handleTouchMove = (event: TouchEvent) => {
    if (event.touches.length !== 1) {
        return
    }

    const container = trackRef.value
    const touch = event.touches[0]
    if (!touch) {
        return
    }

    const deltaX = touch.clientX - touchState.value.startX
    const deltaY = touch.clientY - touchState.value.startY
    const absDeltaX = Math.abs(deltaX)
    const absDeltaY = Math.abs(deltaY)

    if (scale.value > 1) {
        const nextOffsets = clampImageOffsets(
            touchState.value.startOffsetX + deltaX,
            touchState.value.startOffsetY + deltaY,
        )

        imageOffsetX.value = nextOffsets.offsetX
        imageOffsetY.value = nextOffsets.offsetY

        if (container) {
            container.scrollLeft = touchState.value.startIndex * container.clientWidth
        }

        if (event.cancelable) {
            event.preventDefault()
        }

        return
    }

    if (!dismissDragging.value) {
        const shouldStartDismiss = deltaY > 12 && absDeltaY > absDeltaX
        const shouldStartHorizontalDrag = absDeltaX > 8 && absDeltaX > absDeltaY

        if (!horizontalDragging.value && shouldStartDismiss) {
            dismissDragging.value = true
        } else if (!dismissDragging.value && shouldStartHorizontalDrag) {
            horizontalDragging.value = true
        } else if (!dismissDragging.value && !horizontalDragging.value) {
            return
        }
    }

    if (horizontalDragging.value) {
        swipeDeltaX.value = deltaX

        if (container) {
            container.scrollLeft = touchState.value.startScrollLeft - deltaX
        }

        if (event.cancelable) {
            event.preventDefault()
        }

        return
    }

    offsetY.value = Math.max(0, deltaY)

    if (container) {
        container.scrollLeft = touchState.value.startIndex * container.clientWidth
    }

    if (event.cancelable) {
        event.preventDefault()
    }
}

const handleTouchEnd = () => {
    isPointerDown.value = false

    if (horizontalDragging.value) {
        const nextIndex = getSwipeTargetIndex()
        horizontalDragging.value = false
        emit('update:activeIndex', nextIndex)
        scrollToIndex(nextIndex, 'smooth')
        swipeDeltaX.value = 0
        return
    }

    if (!dismissDragging.value) {
        scrollToIndex(safeActiveIndex.value, 'smooth')
        return
    }

    if (offsetY.value > 120) {
        handleClose()
        return
    }

    restorePosition()
}

const handleTouchCancel = () => {
    isPointerDown.value = false

    if (horizontalDragging.value) {
        const nextIndex = getSwipeTargetIndex()
        horizontalDragging.value = false
        emit('update:activeIndex', nextIndex)
        scrollToIndex(nextIndex, 'smooth')
        swipeDeltaX.value = 0
        return
    }

    if (!dismissDragging.value) {
        scrollToIndex(safeActiveIndex.value, 'smooth')
        return
    }

    restorePosition()
}

watch(() => props.visible, (visible) => {
    lockBodyScroll(visible)

    if (!visible) {
        clearScrollEndTimer()
        isPointerDown.value = false
        isSyncingScroll.value = false
        horizontalDragging.value = false
        swipeDeltaX.value = 0
        resetInteraction()
        return
    }

    nextTick(() => {
        scrollToIndex(safeActiveIndex.value)
    })
}, { immediate: true })

watch(() => props.images, (list) => {
    imageRefs.value = imageRefs.value.slice(0, list.length)

    if (!props.visible) {
        return
    }

    if (list.length === 0) {
        handleClose()
        return
    }

    nextTick(() => {
        scrollToIndex(safeActiveIndex.value)
    })
}, { deep: true })

watch(() => props.activeIndex, () => {
    if (!props.visible || isPointerDown.value || isSyncingScroll.value) {
        return
    }

    scale.value = 1
    imageOffsetX.value = 0
    imageOffsetY.value = 0
    nextTick(() => {
        scrollToIndex(safeActiveIndex.value)
    })
})

onUnmounted(() => {
    clearScrollEndTimer()
    lockBodyScroll(false)
})
</script>

<style scoped>
.mobile-image-preview-mask {
    position: fixed;
    inset: 0;
    z-index: 20000;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.96);
}

.mobile-image-preview-close {
    position: absolute;
    top: 16px;
    right: 16px;
    z-index: 1;
    width: 32px;
    height: 32px;
    border: 0;
    padding: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.14);
    color: #fff;
    font-size: 24px;
    line-height: 32px;
}

.mobile-image-preview-download {
    position: absolute;
    right: 16px;
    bottom: 20px;
    z-index: 1;
    width: 36px;
    height: 36px;
    border: 0;
    padding: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.14);
    display: flex;
    align-items: center;
    justify-content: center;
}

.mobile-image-preview-download img {
    width: 20px;
    height: 20px;
}

.mobile-image-preview-track {
    width: 100%;
    height: 100%;
    display: flex;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
}

.mobile-image-preview-track::-webkit-scrollbar {
    display: none;
}

.mobile-image-preview-slide {
    flex: 0 0 100%;
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 48px 16px;
    box-sizing: border-box;
    scroll-snap-align: center;
    scroll-snap-stop: always;
}

.mobile-image-preview-image {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    user-select: none;
    transform-origin: center center;
    will-change: transform;
}

.mobile-image-preview-indicator {
    position: absolute;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    min-width: 52px;
    padding: 4px 10px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.14);
    color: #fff;
    font-size: 12px;
    line-height: 20px;
    text-align: center;
}
</style>