<template>
	<view class="physics-page">
		<view class="top-bar">
			<view class="top-bar-btn" @tap="goBack">
				<text class="nav-glyph">←</text>
			</view>
			<view class="top-bar-title">
				<text class="top-bar-title-text">光栅卡工作室</text>
				<text class="top-bar-subtitle">倾斜手机 · 重力感应预览</text>
			</view>
			<view class="top-bar-btn" @tap="onMore">
				<text class="nav-glyph">⋮</text>
			</view>
		</view>

		<view class="canvas-wrap">
			<view class="canvas-bg" />
			<LenticularCard
				class="preview-stack"
				:layers="layers"
				:transforms="layerTransforms"
				:gyro-source="gyroSourceLabel"
				:skip-built-in-touch="true"
				:tilt-hint-text="tiltHintText"
				@simulate="simulate"
			/>

			<view class="preview-controls" @tap.stop>
				<view class="recalib-chip" @tap.stop="onRecalibrate">
					<text class="recalib-chip-text">校零（水平对准）</text>
				</view>
				<text v-if="gyroHintLine" class="preview-controls-hint">{{ gyroHintLine }}</text>
			</view>
		</view>

		<view class="tool-dock">
			<view class="dock-rim" />
			<view class="layer-strip">
				<view class="layer-strip-header">
					<text class="layer-strip-title">图层素材</text>
					<text class="layer-strip-hint">点击替换 · 长按重置</text>
				</view>
				<view class="layer-slots">
					<view
						v-for="layer in layers"
						:key="layer.id"
						class="layer-slot"
						:class="{ 'layer-slot--filled': !!layer.src }"
						@tap="pickImage(layer.id)"
						@longpress="resetLayer(layer.id)"
					>
						<image v-if="layer.src" class="layer-slot-img" :src="layer.src" mode="aspectFill" />
						<view v-else class="layer-slot-placeholder" :style="placeholderStyle(layer)">
							<text class="layer-slot-icon">＋</text>
						</view>
						<view class="layer-slot-meta">
							<text class="layer-slot-name">{{ layer.label }}</text>
							<text class="layer-slot-tag">{{ layer.src ? '已上传' : '点击上传' }}</text>
						</view>
						<view v-if="layer.src" class="layer-slot-badge" @tap.stop="resetLayer(layer.id)">
							<text class="layer-slot-badge-icon">×</text>
						</view>
					</view>
				</view>
			</view>
		</view>
	</view>
</template>

<script>
export default {
	inheritAttrs: false,
};
</script>

<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import LenticularCard from '@/components/lenticular/LenticularCard.vue'
import { useLenticularPreview } from '@/composables/useLenticularPreview.js'
import {
	buildLenticularLayersTwo,
	LENTICULAR_STUDIO_STORAGE_KEY,
} from '@/utils/castloveMintForm.js'

const layers = ref([])
const gyroSourceLabel = ref('simulation')

let accelHandler = null
let accelSmoothed = 0
/** 平面内重力方向角增量积分，用于持续同向倾斜时周期性叠化 */
let rollAccum = 0
let prevCu = null
let prevCv = null

/** 基线帧数：过长时用户若在采样期小幅晃动，会把「水平」拉进摆动中间，相对角变小、体感发木 */
const BASELINE_FRAMES = 10
/** 瞬时相对基线角 → [-1,1] 的弧度尺度（越小越灵敏） */
const INSTANT_FULL_RAD = 0.32
/** 相邻帧转角上限（弧度），抑制噪声尖峰 */
const MAX_STEP_RAD = 0.48
/** 角增量放大：越大同向拧动积分越快、周期切换越快 */
const STEP_GAIN = 2.15
/** sin 相位倍率：越大同样积分下周期越多 */
const SIN_PHASE_SCALE = 1.18
/** 输出 = 瞬时项 + 周期项 的混合 */
const BLEND_INSTANT = 0.38

let tiltCal = {
	lockedMode: null,
	planeVotes: [],
	sumU: 0,
	sumV: 0,
	count: 0,
	ready: false,
	bu: 1,
	bv: 0,
}

function resetTiltCalibration() {
	tiltCal = {
		lockedMode: null,
		planeVotes: [],
		sumU: 0,
		sumV: 0,
		count: 0,
		ready: false,
		bu: 1,
		bv: 0,
	}
}

function majorityPlane(votes) {
	const cnt = { xz: 0, xy: 0, yz: 0 }
	for (const v of votes) cnt[v] = (cnt[v] || 0) + 1
	if (cnt.xz >= cnt.xy && cnt.xz >= cnt.yz) return 'xz'
	if (cnt.xy >= cnt.yz) return 'xy'
	return 'yz'
}

/** 重力主要沿哪条设备轴：在该轴上投影小，用另两轴构成平面测「绕该轴的滚转」 */
function detectGravityPlane(nx, ny, nz) {
	const ax = Math.abs(nx)
	const ay = Math.abs(ny)
	const az = Math.abs(nz)
	if (ay >= ax && ay >= az) return 'xz'
	if (az >= ax && az >= ay) return 'xy'
	return 'yz'
}

function uvForPlane(nx, ny, nz, mode) {
	if (mode === 'xz') return { u: nx, v: nz }
	if (mode === 'xy') return { u: nx, v: ny }
	return { u: ny, v: nz }
}

/**
 * @returns {null | { warmup: boolean, cu?: number, cv?: number, deltaRad?: number }}
 */
function accelToRelativeTilt01(ax, ay, az) {
	const gMag = Math.hypot(ax, ay, az)
	if (gMag < 0.18) return null
	const nx = ax / gMag
	const ny = ay / gMag
	const nz = az / gMag

	if (!tiltCal.ready) {
		const m = detectGravityPlane(nx, ny, nz)
		if (tiltCal.planeVotes.length < 5) tiltCal.planeVotes.push(m)
		if (tiltCal.planeVotes.length >= 5 && tiltCal.lockedMode == null) {
			tiltCal.lockedMode = majorityPlane(tiltCal.planeVotes)
			tiltCal.sumU = 0
			tiltCal.sumV = 0
			tiltCal.count = 0
		}
		if (tiltCal.lockedMode == null) return { warmup: true }
		const { u, v } = uvForPlane(nx, ny, nz, tiltCal.lockedMode)
		const h = Math.hypot(u, v)
		if (h < 0.035) return { warmup: true }
		tiltCal.sumU += u / h
		tiltCal.sumV += v / h
		tiltCal.count += 1
		if (tiltCal.count >= BASELINE_FRAMES) {
			const bh = Math.hypot(tiltCal.sumU, tiltCal.sumV) || 1
			tiltCal.bu = tiltCal.sumU / bh
			tiltCal.bv = tiltCal.sumV / bh
			tiltCal.ready = true
		}
		return { warmup: true }
	}

	const { u, v } = uvForPlane(nx, ny, nz, tiltCal.lockedMode)
	const h = Math.hypot(u, v)
	if (h < 0.028) return null
	const cu = u / h
	const cv = v / h
	const sinD = cu * tiltCal.bv - cv * tiltCal.bu
	const cosD = cu * tiltCal.bu + cv * tiltCal.bv
	const deltaRad = Math.atan2(sinD, cosD)
	return { warmup: false, cu, cv, deltaRad }
}

const { physics, layerTransforms, simulate } = useLenticularPreview(layers)

const tiltHintText = computed(() => '缓慢扭动手机 · 「校零」回中')

const gyroHintLine = computed(() => {
	if (gyroSourceLabel.value === 'accelerometer') {
		return '重力计驱动 · 持续同向倾斜会反复叠化；可点「校零」'
	}
	return '倾斜驱动预览中；若无效请检查权限或换真机'
})

function goBack() {
	uni.navigateBack({
		delta: 1,
		fail: () => {},
	})
}

function onMore() {
	uni.showActionSheet({
		itemList: ['恢复为进入工作室时的图片'],
		success: (res) => {
			if (res.tapIndex === 0) {
				const raw = uni.getStorageSync(LENTICULAR_STUDIO_STORAGE_KEY)
				if (!raw) return
				try {
					const p = typeof raw === 'string' ? JSON.parse(raw) : raw
					layers.value = buildLenticularLayersTwo(p.bgPath || '', p.subjectPath || '')
					uni.showToast({ title: '已恢复', icon: 'none' })
				} catch (e) {
					console.error(e)
				}
			}
		},
	})
}

function placeholderStyle(layer) {
	if (layer.background) {
		return { background: layer.background }
	}
	return { background: 'rgba(6,14,32,0.55)' }
}

function pickImage(layerId) {
	uni.chooseImage({
		count: 1,
		sizeType: ['compressed'],
		sourceType: ['album', 'camera'],
		success: (res) => {
			const path = res.tempFilePaths && res.tempFilePaths[0]
			if (!path) return
			const idx = layers.value.findIndex((l) => l.id === layerId)
			if (idx < 0) return
			const cur = layers.value[idx]
			const next = { ...cur, src: path, background: undefined }
			if (layerId === 'base') next.dots = undefined
			const copy = [...layers.value]
			copy[idx] = next
			layers.value = copy
		},
		fail: (err) => {
			const msg = (err && err.errMsg) || ''
			if (!msg.includes('cancel')) {
				uni.showToast({ title: '选择图片失败', icon: 'none' })
			}
		},
	})
}

function resetLayer(layerId) {
	const fresh = buildLenticularLayersTwo('', '')
	const defLayer = fresh.find((l) => l.id === layerId)
	const idx = layers.value.findIndex((l) => l.id === layerId)
	if (idx < 0 || !defLayer) return
	const copy = [...layers.value]
	copy[idx] = { ...defLayer }
	layers.value = copy
}

function stopAccel() {
	try {
		if (accelHandler && typeof uni.offAccelerometerChange === 'function') {
			uni.offAccelerometerChange(accelHandler)
		}
	} catch (e) {
		/* noop */
	}
	accelHandler = null
	try {
		uni.stopAccelerometer({})
	} catch (e) {
		/* noop */
	}
}

function startAccel() {
	stopAccel()
	resetTiltCalibration()
	accelSmoothed = 0
	rollAccum = 0
	prevCu = null
	prevCv = null
	accelHandler = (res) => {
		const ax = Number(res.x) || 0
		const ay = Number(res.y) || 0
		const az = Number(res.z) || 0
		const out = accelToRelativeTilt01(ax, ay, az)
		if (out == null) return
		if (out.warmup) return
		const { cu, cv, deltaRad } = out
		if (prevCu != null && prevCv != null) {
			let step = Math.atan2(cu * prevCv - cv * prevCu, cu * prevCu + cv * prevCv)
			if (step > Math.PI * 0.92) step -= 2 * Math.PI
			if (step < -Math.PI * 0.92) step += 2 * Math.PI
			step = Math.max(-MAX_STEP_RAD, Math.min(MAX_STEP_RAD, step))
			rollAccum += step * STEP_GAIN
		}
		prevCu = cu
		prevCv = cv

		const instant = Math.max(-1, Math.min(1, deltaRad / INSTANT_FULL_RAD))
		const cyclic = Math.sin(rollAccum * SIN_PHASE_SCALE)
		const target = BLEND_INSTANT * instant + (1 - BLEND_INSTANT) * cyclic

		accelSmoothed += (target - accelSmoothed) * 0.62
		simulate(Math.max(-1, Math.min(1, accelSmoothed)), 0)
		gyroSourceLabel.value = 'accelerometer'
	}
	try {
		uni.onAccelerometerChange(accelHandler)
		const applyStart = (interval) => {
			uni.startAccelerometer({
				interval,
				success: () => {
					gyroSourceLabel.value = 'accelerometer'
				},
				fail: () => {
					if (interval === 'game') {
						applyStart('normal')
						return
					}
					gyroSourceLabel.value = 'simulation'
					uni.showToast({ title: '无法启动加速度计', icon: 'none' })
				},
			})
		}
		applyStart('game')
	} catch (e) {
		gyroSourceLabel.value = 'simulation'
	}
}

function onRecalibrate() {
	resetTiltCalibration()
	accelSmoothed = 0
	rollAccum = 0
	prevCu = null
	prevCv = null
	simulate(0, 0)
	uni.showToast({ title: '已重置水平基准', icon: 'none' })
}

onMounted(() => {
	try {
		const raw = uni.getStorageSync(LENTICULAR_STUDIO_STORAGE_KEY)
		if (!raw) {
			uni.showToast({ title: '缺少素材数据，请返回上一步', icon: 'none' })
			setTimeout(() => goBack(), 1600)
			return
		}
		const p = typeof raw === 'string' ? JSON.parse(raw) : raw
		layers.value = buildLenticularLayersTwo(p.bgPath || '', p.subjectPath || '')
	} catch (e) {
		console.error('[lenticular-studio] load payload', e)
		uni.showToast({ title: '数据无效', icon: 'none' })
		setTimeout(() => goBack(), 1600)
		return
	}
	nextTick(() => {
		physics.angleStability = 16
		physics.transitionSmoothness = 62
		physics.tiltSensitivity = 96
		physics.sensorDeadzoneStrength = 0
		startAccel()
	})
})

onUnmounted(() => {
	stopAccel()
})
</script>

<style>
.nav-glyph {
	font-size: 22px;
	line-height: 1;
	font-weight: 600;
	color: inherit;
}
</style>

<style scoped>
.physics-page {
	width: 100vw;
	height: 100vh;
	display: flex;
	flex-direction: column;
	background: #0b1326;
	color: #dae2fd;
	overflow: hidden;
	position: relative;
}

.top-bar {
	position: relative;
	z-index: 50;
	display: flex;
	justify-content: space-between;
	align-items: center;
	padding: 0 16px;
	height: 56px;
	padding-top: calc(env(safe-area-inset-top) + 4px);
	background: rgba(11, 19, 38, 0.88);
	border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}

.top-bar-btn {
	width: 44px;
	height: 44px;
	display: flex;
	align-items: center;
	justify-content: center;
	color: #ddb7ff;
}

.top-bar-title {
	display: flex;
	flex-direction: column;
	align-items: center;
}

.top-bar-title-text {
	font-size: 18px;
	font-weight: 600;
	color: #dae2fd;
}

.top-bar-subtitle {
	font-size: 11px;
	font-weight: 600;
	letter-spacing: 0.16em;
	color: #4cd7f6;
	margin-top: 2px;
}

.canvas-wrap {
	flex: 1;
	min-height: 0;
	position: relative;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: 8px 10px 4px;
	z-index: 10;
}

.preview-stack {
	position: relative;
	z-index: 1;
	width: 100%;
	height: 100%;
	min-height: 0;
	display: flex;
	align-items: center;
	justify-content: center;
}

.preview-controls {
	position: absolute;
	top: 10px;
	left: 50%;
	transform: translateX(-50%);
	z-index: 22;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 8px;
	max-width: 92vw;
}

.recalib-chip {
	padding: 5px 14px;
	border-radius: 999px;
	background: rgba(45, 52, 73, 0.82);
	border: 1px solid rgba(255, 255, 255, 0.12);
}

.recalib-chip-text {
	font-size: 11px;
	font-weight: 600;
	color: #ddb7ff;
}

.preview-controls-hint {
	font-size: 10px;
	color: rgba(207, 194, 214, 0.78);
	text-align: center;
	max-width: 260px;
	line-height: 1.45;
}

.canvas-bg {
	position: absolute;
	inset: 0;
	background:
		radial-gradient(ellipse at 50% 35%, rgba(132, 43, 210, 0.18), transparent 55%),
		radial-gradient(ellipse at 50% 85%, rgba(76, 215, 246, 0.1), transparent 60%);
	pointer-events: none;
}

.tool-dock {
	position: relative;
	z-index: 20;
	margin: 0 10px calc(10px + env(safe-area-inset-bottom));
	max-width: 460px;
	align-self: center;
	width: calc(100% - 20px);
	background: rgba(34, 42, 61, 0.88);
	border: 1px solid rgba(77, 67, 84, 0.45);
	border-radius: 20px;
	padding: 12px 14px;
	display: flex;
	flex-direction: column;
	gap: 10px;
	box-shadow: 0 18px 50px rgba(0, 0, 0, 0.4);
	overflow: hidden;
}

.dock-rim {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	height: 1px;
	background: linear-gradient(to right, transparent, rgba(221, 183, 255, 0.45), transparent);
}

.layer-strip {
	display: flex;
	flex-direction: column;
	gap: 8px;
}

.layer-strip-header {
	display: flex;
	justify-content: space-between;
	align-items: center;
	padding: 0 2px;
}

.layer-strip-title {
	font-size: 13px;
	font-weight: 600;
	color: #dae2fd;
}

.layer-strip-hint {
	font-size: 11px;
	color: rgba(207, 194, 214, 0.6);
}

.layer-slots {
	display: flex;
	gap: 8px;
}

.layer-slot {
	flex: 1;
	position: relative;
	display: flex;
	flex-direction: column;
	border-radius: 12px;
	overflow: hidden;
	background: rgba(6, 14, 32, 0.55);
	border: 1px solid rgba(255, 255, 255, 0.08);
}

.layer-slot--filled {
	border-color: rgba(76, 215, 246, 0.5);
	box-shadow: 0 0 14px rgba(76, 215, 246, 0.15);
}

.layer-slot-img {
	width: 100%;
	height: 56px;
	display: block;
}

.layer-slot-placeholder {
	width: 100%;
	height: 56px;
	display: flex;
	align-items: center;
	justify-content: center;
	background-size: cover;
	background-position: center;
}

.layer-slot-icon {
	font-size: 22px;
	color: rgba(221, 183, 255, 0.85);
}

.layer-slot-meta {
	display: flex;
	flex-direction: column;
	padding: 6px 8px 8px;
	gap: 2px;
	background: rgba(6, 14, 32, 0.55);
}

.layer-slot-name {
	font-size: 12px;
	font-weight: 600;
	color: #dae2fd;
}

.layer-slot-tag {
	font-size: 10px;
	color: rgba(207, 194, 214, 0.7);
}

.layer-slot--filled .layer-slot-tag {
	color: #4cd7f6;
}

.layer-slot-badge {
	position: absolute;
	top: 4px;
	right: 4px;
	width: 22px;
	height: 22px;
	display: flex;
	align-items: center;
	justify-content: center;
	border-radius: 50%;
	background: rgba(0, 0, 0, 0.55);
	border: 1px solid rgba(255, 255, 255, 0.18);
}

.layer-slot-badge-icon {
	font-size: 14px;
	color: #dae2fd;
}
</style>
