<template>
	<view class="pull-refresh" :style="wrapStyle">
		<view
			class="pull-refresh__zone"
			:class="{ 'pull-refresh__zone--no-transition': pulling }"
			:style="refreshZoneStyle"
		>
			<view
				class="pull-refresh__circle-wrap"
				:class="{ 'pull-refresh__circle-wrap--no-transition': pulling }"
				:style="circleWrapStyle"
			>
				<view class="pull-refresh__circle" :style="circleInnerStyle" />
			</view>
		</view>
		<scroll-view
			:class="['pull-refresh__scroll', scrollClass]"
			:scroll-y="scrollEnabled && !pulling"
			:show-scrollbar="showScrollbar"
			:scroll-with-animation="false"
			:lower-threshold="lowerThreshold"
			@scroll="onScroll"
			@touchstart="onPullStart"
			@touchmove="onPullMove"
			@touchend="onPullEnd"
			@touchcancel="onPullEnd"
			@scrolltolower="onScrollToLower"
		>
			<slot />
			<!-- 底部加载状态（footerMode 时显示），预留 footerPaddingBottom 避免被底栏/安全区遮挡 -->
			<view
				v-if="footerMode"
				class="pull-refresh__footer"
				:style="footerStyle"
			>
				<view v-if="loading && !isRefreshing && !(noEmptyFooter && listLength === 0)" class="pull-refresh__footer-text">加载中...</view>
				<view v-else-if="noMore && listLength > 0" class="pull-refresh__footer-text">已经到底啦</view>
				<view v-else-if="listLength === 0 && loaded && !noEmptyFooter" class="pull-refresh__footer-text">还没有动态</view>
			</view>
		</scroll-view>
	</view>
</template>

<script setup lang="ts">
/**
 * PullRefresh 下拉刷新
 * 用法：当成Scroll组件用然后包住内容，监听 @refresh 拉取数据，拉完后设 :is-refreshing="false"
 * pullMax 下拉最大 px；circleOffsetStart / circleOffsetEnd 圆圈起止位置
 */
import { ref, computed, watch, onUnmounted } from 'vue'

const props = withDefaults(
	defineProps<{
		isRefreshing?: boolean
		scrollClass?: string
		showScrollbar?: boolean
		height?: string
		/** 下拉最多允许超出多少 px，超过后刷新圈不再继续往下拉，默认 160 */
		pullMax?: number
		circleOffsetStart?: string
		circleOffsetEnd?: string
		/** 启用底部加载状态 UI（加载中/已经到底啦/还没有动态） */
		footerMode?: boolean
		/** 触底加载中 */
		loading?: boolean
		/** 已加载完，无更多 */
		noMore?: boolean
		/** 列表长度，用于空态判断 */
		listLength?: number
		/** 首屏已加载完成，用于空态判断 */
		loaded?: boolean
		/** 触底检测距离（px），默认 100 */
		lowerThreshold?: number
		/** 底部预留（如 tab 栏+安全区），避免 footer 文字被遮挡，如 "calc(120rpx + env(safe-area-inset-bottom))" */
		footerPaddingBottom?: string
		/** 为 true 时不显示「还没有动态」空态（用于详情页等自有空态的场景） */
		noEmptyFooter?: boolean
		/** 是否允许纵向滚动 */
		scrollEnabled?: boolean
	}>(),
	{
		isRefreshing: false,
		scrollClass: '',
		showScrollbar: false,
		height: '100%',
		scrollEnabled: true,
		pullMax: 60,
		circleOffsetStart: '0rpx',
		circleOffsetEnd: '36rpx',
		footerMode: false,
		loading: false,
		noMore: false,
		listLength: 0,
		loaded: true,
		lowerThreshold: 100,
		footerPaddingBottom: '0rpx',
		noEmptyFooter: false,
	}
)

const emit = defineEmits<{
	(e: 'refresh'): void
	(e: 'scroll', ev: any): void
	(e: 'scrolltolower', ev: any): void
	(e: 'touchstart'): void
	(e: 'touchend'): void
}>()

// --- 常量  ---
const CONFIG = {
	PULL_TRIGGER: 56,
	PULL_DAMPING: 0.82,
	ZONE_HEIGHT_PX: 50,
	LERP_FACTOR: 0.62,
	SPIN_DURATION_MS: 700,
	SPIN_INTERVAL_MS: 16,
	PULL_CAP: 500,
	/** 下拉时旋转阻尼，0~1，越小拉同样距离转得越少，默认 0.35（约拉满 pullMax 转 1/3 圈） */
	ROTATION_DAMPING: 0.35
} as const

const SPIN_STEP = (360 / CONFIG.SPIN_DURATION_MS) * CONFIG.SPIN_INTERVAL_MS

// --- 状态 ---
const scrollTop = ref(0)
const pullDistance = ref(0)
const pulling = ref(false)
const refreshStartRotation = ref(0)
const spinDeg = ref(0)

let touchStartY = 0
let touchStartScrollTop = 0
let spinTimerId: number | ReturnType<typeof setInterval> = 0

// --- 工具 ---
const lerp = (a: number, b: number, t: number) => a + (b - a) * t

const getTouchY = (e: any): number | null => e.touches?.[0]?.clientY ?? null

function clearSpinTimer() {
	if (spinTimerId) {
		clearInterval(spinTimerId)
		spinTimerId = 0
	}
}

// --- 计算属性 ---
const wrapStyle = computed(() => ({ height: props.height }))

/** 展示进度：按实际下拉量 / pullMax，可>1，用于缩放/透明度/旋转（位移单独用 pullDistance  capped） */
const revealProgress = computed(() => {
	if (props.isRefreshing) return 1
	return pullDistance.value / Math.max(1, props.pullMax)
})

/** 位移在 pullMax 处封顶，再往下拉只动动画不动位置 */
const refreshZoneStyle = computed(() => {
	const displacement = Math.min(pullDistance.value, props.pullMax)
	const y = props.isRefreshing ? 0 : -CONFIG.ZONE_HEIGHT_PX + displacement
	return { transform: `translateY(${y}px)` }
})

const circleWrapStyle = computed(() => {
	const p = revealProgress.value
	return {
		marginTop: props.isRefreshing ? props.circleOffsetEnd : props.circleOffsetStart,
		transform: `scale(${Math.min(1, 0.3 + p * 0.7)})`,
		opacity: Math.min(1, p),
		transformOrigin: 'center center'
	}
})

const circleInnerStyle = computed(() => {
	const deg = props.isRefreshing
		? spinDeg.value
		: revealProgress.value * 360 * CONFIG.ROTATION_DAMPING
	return { transform: `rotate(${deg}deg)` }
})

const footerStyle = computed(() =>
	props.footerPaddingBottom ? { paddingBottom: props.footerPaddingBottom } : {}
)

// --- 触摸与滚动 ---
function onPullStart(e: any) {
	emit('touchstart')
	const y = getTouchY(e)
	if (y == null) return
	touchStartY = y
	touchStartScrollTop = scrollTop.value
	pulling.value = false
}

function onPullMove(e: any) {
	const y = getTouchY(e)
	if (y == null) return
	const dy = y - touchStartY
	if (touchStartScrollTop > 0 || dy <= 0) return
	pulling.value = true
	e.preventDefault?.()
	const target = Math.min(CONFIG.PULL_CAP, dy * CONFIG.PULL_DAMPING)
	pullDistance.value = lerp(pullDistance.value, target, CONFIG.LERP_FACTOR)
}

function onPullEnd() {
	emit('touchend')
	pulling.value = false
	if (pullDistance.value >= CONFIG.PULL_TRIGGER) {
		refreshStartRotation.value =
			(pullDistance.value / Math.max(1, props.pullMax)) * 360 * CONFIG.ROTATION_DAMPING
		emit('refresh')
	} else {
		pullDistance.value = 0
	}
}

function onScroll(e: any) {
	scrollTop.value = e?.detail?.scrollTop ?? 0
	emit('scroll', e)
}

function onScrollToLower(e: any) {
	emit('scrolltolower', e)
}

// --- 刷新中旋转 ---
watch(
	() => props.isRefreshing,
	(cur, prev) => {
		if (cur && !prev) {
			spinDeg.value = refreshStartRotation.value
			spinTimerId = setInterval(() => {
				spinDeg.value += SPIN_STEP
			}, CONFIG.SPIN_INTERVAL_MS)
		} else if (!cur && prev) {
			clearSpinTimer()
			pullDistance.value = 0
		}
	}
)

onUnmounted(clearSpinTimer)
</script>

<style lang="scss" scoped>
.pull-refresh {
	position: relative;
	display: flex;
	flex-direction: column;
	box-sizing: border-box;
}

.pull-refresh__zone {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	height: 100rpx;
	display: flex;
	align-items: flex-start;
	justify-content: center;
	z-index: 10;
	pointer-events: none;
	transition: transform 0.25s ease-out;

	&--no-transition {
		transition: none;
	}
}

.pull-refresh__circle-wrap {
	transition: opacity 0.2s ease, margin-top 0.25s ease-out, transform 0.2s ease-out;

	&--no-transition {
		transition: none;
	}
}

.pull-refresh__circle {
	width: 36rpx;
	height: 36rpx;
	border-radius: 50%;
	border: 6rpx solid var(--dv-border);
	border-top-color: var(--dv-accent);
}

.pull-refresh__scroll {
	flex: 1;
	height: 0;
	display: flex;
	flex-direction: column;
	box-sizing: border-box;
	background-color: var(--dv-bg);
}

.pull-refresh__footer {
	text-align: center;
	position: relative;
	z-index: 0;
}
.pull-refresh__footer-text {
	font-size: var(--dv-font-muted);
	color: var(--dv-text-muted);
}
</style>
