<template>
  <div class="cad-model-viewer">
    <div ref="containerRef" class="cad-model-viewer__canvas" />
    <div v-if="loading" class="cad-model-viewer__overlay">
      <n-spin size="large" />
      <span>模型加载中…</span>
    </div>
    <div v-else-if="error" class="cad-model-viewer__overlay cad-model-viewer__overlay--error">
      <n-empty :description="error" />
    </div>
    <div v-if="!loading && !error" class="cad-model-viewer__toolbar">
      <n-button size="tiny" quaternary @click="resetView">复位</n-button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
import { NButton, NSpin, NEmpty } from 'naive-ui'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { createGLTFLoader, disposeDracoLoader } from '@/utils/gltfLoader'
import { useUserStore } from '@/stores/user'

const props = defineProps<{
  /** 远程 glb/gltf 地址（需登录时可带 Token） */
  url?: string
  /** 本地文件（与 url 二选一，file 优先） */
  file?: File | null
}>()

const containerRef = ref<HTMLDivElement | null>(null)
const loading = ref(false)
const error = ref('')

let renderer: THREE.WebGLRenderer | null = null
let scene: THREE.Scene | null = null
let camera: THREE.PerspectiveCamera | null = null
let controls: OrbitControls | null = null
let animationId = 0
let modelRoot: THREE.Object3D | null = null
let defaultCameraPosition = new THREE.Vector3(2, 2, 2)
let defaultTarget = new THREE.Vector3(0, 0, 0)
let disposed = false
let objectUrls: string[] = []

function resolveUrl(url: string) {
  if (url.startsWith('http://') || url.startsWith('https://')) return url
  if (url.startsWith('/')) return `${window.location.origin}${url}`
  return url
}

async function fetchModelArrayBuffer(url: string): Promise<ArrayBuffer> {
  const userStore = useUserStore()
  const headers: Record<string, string> = {}
  if (userStore.token) headers.Authorization = userStore.token

  const res = await fetch(resolveUrl(url), { headers })
  if (!res.ok) throw new Error(`模型加载失败 (${res.status})`)
  return res.arrayBuffer()
}

function initScene() {
  const container = containerRef.value
  if (!container) return

  const width = container.clientWidth || 640
  const height = container.clientHeight || 480

  scene = new THREE.Scene()
  scene.background = new THREE.Color(0xf0f2f5)

  camera = new THREE.PerspectiveCamera(45, width / height, 0.01, 10000)
  camera.position.copy(defaultCameraPosition)

  renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
  renderer.setSize(width, height)
  renderer.outputColorSpace = THREE.SRGBColorSpace
  container.innerHTML = ''
  container.appendChild(renderer.domElement)

  controls = new OrbitControls(camera, renderer.domElement)
  controls.enableDamping = true
  controls.dampingFactor = 0.08
  controls.target.copy(defaultTarget)

  const ambient = new THREE.AmbientLight(0xffffff, 0.75)
  const dir1 = new THREE.DirectionalLight(0xffffff, 1.1)
  dir1.position.set(5, 8, 6)
  const dir2 = new THREE.DirectionalLight(0xffffff, 0.45)
  dir2.position.set(-4, -2, -5)
  scene.add(ambient, dir1, dir2)

  const grid = new THREE.GridHelper(10, 20, 0xcccccc, 0xe8e8e8)
  scene.add(grid)

  animate()
}

function animate() {
  animationId = requestAnimationFrame(animate)
  controls?.update()
  if (renderer && scene && camera) renderer.render(scene, camera)
}

function frameModel(object: THREE.Object3D) {
  if (!camera || !controls) return
  const box = new THREE.Box3().setFromObject(object)
  if (box.isEmpty()) return

  const size = box.getSize(new THREE.Vector3())
  const center = box.getCenter(new THREE.Vector3())
  const maxDim = Math.max(size.x, size.y, size.z, 0.001)
  const dist = maxDim * 1.8 / Math.tan((camera.fov * Math.PI) / 360)

  defaultTarget.copy(center)
  defaultCameraPosition.set(center.x + dist * 0.6, center.y + dist * 0.5, center.z + dist * 0.8)
  camera.position.copy(defaultCameraPosition)
  camera.near = maxDim / 100
  camera.far = maxDim * 100
  camera.updateProjectionMatrix()
  controls.target.copy(center)
  controls.update()
}

function resetView() {
  if (!camera || !controls) return
  camera.position.copy(defaultCameraPosition)
  controls.target.copy(defaultTarget)
  controls.update()
}

function clearModel() {
  if (modelRoot && scene) {
    scene.remove(modelRoot)
    modelRoot.traverse((child) => {
      const mesh = child as THREE.Mesh
      if (mesh.geometry) mesh.geometry.dispose()
      if (mesh.material) {
        const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
        mats.forEach(m => m.dispose())
      }
    })
    modelRoot = null
  }
  objectUrls.forEach(u => URL.revokeObjectURL(u))
  objectUrls = []
}

function disposeViewer() {
  disposed = true
  cancelAnimationFrame(animationId)
  clearModel()
  controls?.dispose()
  renderer?.dispose()
  controls = null
  renderer = null
  scene = null
  camera = null
  disposeDracoLoader()
}

async function loadFromFile(file: File) {
  if (disposed) return
  loading.value = true
  error.value = ''
  clearModel()

  const ext = file.name.split('.').pop()?.toLowerCase() ?? ''
  if (ext !== 'glb' && ext !== 'gltf') {
    error.value = '仅支持 .glb / .gltf 文件'
    loading.value = false
    return
  }

  try {
    if (!scene) initScene()
    const loader = createGLTFLoader()

    if (ext === 'glb') {
      const buffer = await file.arrayBuffer()
      const gltf = await new Promise<{ scene: THREE.Object3D }>((resolve, reject) => {
        loader.parse(buffer, '', resolve, reject)
      })
      modelRoot = gltf.scene
    } else {
      const objectUrl = URL.createObjectURL(file)
      objectUrls.push(objectUrl)
      const gltf = await new Promise<{ scene: THREE.Object3D }>((resolve, reject) => {
        loader.load(objectUrl, (g) => resolve(g), undefined, reject)
      })
      modelRoot = gltf.scene
    }

    scene!.add(modelRoot!)
    frameModel(modelRoot!)
  } catch (e) {
    error.value = ext === 'gltf'
      ? 'gltf 加载失败；若含外部 .bin 请改用单文件 .glb'
      : (e instanceof Error ? e.message : '模型加载失败')
  } finally {
    loading.value = false
  }
}

async function loadModel(url: string) {
  if (!url || disposed) return
  loading.value = true
  error.value = ''
  clearModel()

  try {
    if (!scene) initScene()
    const buffer = await fetchModelArrayBuffer(url)
    const loader = createGLTFLoader()
    const gltf = await new Promise<{ scene: THREE.Object3D }>((resolve, reject) => {
      loader.parse(buffer, '', resolve, reject)
    })
    modelRoot = gltf.scene
    scene!.add(modelRoot)
    frameModel(modelRoot)
  } catch (e) {
    error.value = e instanceof Error ? e.message : '模型加载失败'
  } finally {
    loading.value = false
  }
}

function onResize() {
  const container = containerRef.value
  if (!container || !renderer || !camera) return
  const width = container.clientWidth
  const height = container.clientHeight
  if (!width || !height) return
  camera.aspect = width / height
  camera.updateProjectionMatrix()
  renderer.setSize(width, height)
}

watch(
  () => [props.file, props.url] as const,
  ([file, url]) => {
    if (file) {
      loadFromFile(file)
    } else if (url) {
      loadModel(url)
    }
  },
  { immediate: true },
)

onMounted(() => {
  window.addEventListener('resize', onResize)
})

onBeforeUnmount(() => {
  window.removeEventListener('resize', onResize)
  disposeViewer()
})
</script>

<style scoped>
.cad-model-viewer {
  position: relative;
  width: 100%;
  height: 100%;
  min-height: 420px;
  background: #f0f2f5;
  border-radius: 8px;
  overflow: hidden;
}

.cad-model-viewer__canvas {
  width: 100%;
  height: 100%;
  min-height: 420px;
}

.cad-model-viewer__canvas :deep(canvas) {
  display: block;
  width: 100% !important;
  height: 100% !important;
}

.cad-model-viewer__overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  background: rgba(255, 255, 255, 0.85);
  z-index: 2;
}

.cad-model-viewer__overlay--error {
  background: rgba(255, 255, 255, 0.92);
}

.cad-model-viewer__toolbar {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 3;
}
</style>
