<template>
  <div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
    <h3 class="text-lg font-semibold mb-4 text-blue-400">V5: 3D Space-Time Cube</h3>
    <div class="text-xs text-gray-500 mb-3 flex gap-2 items-center">
      <span>X/Y = 畫面位置</span>
      <span>Z = 深度（bbox 大小）</span>
      <span>T = 時間</span>
    </div>

    <!-- Trace selector -->
    <div class="flex gap-2 mb-3">
      <select v-model="selectedTraceId"
              class="bg-gray-700 text-white px-3 py-1.5 rounded text-sm flex-1">
        <option :value="null" disabled>選擇 Trace</option>
        <option v-for="t in traceOptions" :key="t.id"
                :value="t.id">{{ t.label }}</option>
      </select>
      <button @click="loadData"
              class="bg-blue-600 hover:bg-blue-500 text-white px-4 py-1.5 rounded text-sm"
              :disabled="!selectedTraceId || loading">
        {{ loading ? '載入中...' : '載入' }}
      </button>
    </div>

    <div ref="container" class="w-full h-[400px] bg-gray-900 rounded-lg overflow-hidden"></div>

    <div class="text-xs text-gray-500 mt-2 flex gap-4">
      <span>🖱 拖曳旋轉</span>
      <span>🔍 滾輪縮放</span>
      <span v-if="faceCount">{{ faceCount }} 個檢測點</span>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { httpFetch, getCurrentConfig } from '@/api/client'

const props = defineProps<{
  fileUuid: string
  traces?: any[]
  frameWidth?: number
  frameHeight?: number
}>()

const container = ref<HTMLElement>()
const selectedTraceId = ref<number | null>(null)
const loading = ref(false)
const faceCount = ref(0)

const traceOptions = computed(() => {
  return (props.traces || []).map((t: any) => ({
    id: t.trace_id,
    label: `#${t.trace_id} (${t.face_count} faces, ${(t.duration_sec || 0).toFixed(1)}s)`
  }))
})

let renderer: THREE.WebGLRenderer | null = null
let scene: THREE.Scene | null = null
let camera: THREE.PerspectiveCamera | null = null
let controls: OrbitControls | null = null
let animId: number
let objects: THREE.Object3D[] = []

function disposeScene() {
  cancelAnimationFrame(animId)
  for (const obj of objects) {
    scene?.remove(obj)
    if (obj instanceof THREE.Mesh || obj instanceof THREE.Points || obj instanceof THREE.Line) {
      obj.geometry?.dispose()
      const mat = (obj as any).material
      if (mat) {
        if (Array.isArray(mat)) mat.forEach((m: any) => m.dispose())
        else mat.dispose()
      }
    }
  }
  objects = []
  controls?.dispose()
  controls = null
  if (renderer) {
    renderer.dispose()
    renderer = null
  }
  scene = null
  camera = null
}

type FacePoint = {
  frame: number
  t: number
  x: number
  y: number
  w: number
  h: number
  z: number
}

function loadData() {
  if (!selectedTraceId.value) return
  loading.value = true

  const config = getCurrentConfig()
  httpFetch(`${config.api_base_url}/api/v1/file/${props.fileUuid}/trace/${selectedTraceId.value}/faces?interpolate=true&limit=2000&dimension=3d`)
    .then((res: any) => {
      const faces = res?.faces || []
      const fw = props.frameWidth || 1920
      const fh = props.frameHeight || 1080

      const points: FacePoint[] = faces.map((f: any) => {
        const w = f.width || 1
        const h = f.height || 1
        const areaPct = (w * h) / (fw * fh)
        const z = f.z_rel !== undefined && f.z_rel !== null
          ? f.z_rel
          : 1.0 - Math.min(areaPct * 50, 1.0)
        return {
          frame: f.start_frame || 0,
          t: f.start_time || 0,
          x: f.x || 0,
          y: f.y || 0,
          w,
          h,
          z
        }
      })
      faceCount.value = points.length
      buildScene(points)
    })
    .catch((err: any) => {
      console.error('Failed to load trace faces:', err)
    })
    .finally(() => {
      loading.value = false
    })
}

function buildScene(points: FacePoint[]) {
  if (!container.value) return
  disposeScene()

  // Normalize coordinates to [-1, 1] range
  const fw = props.frameWidth || 1920
  const fh = props.frameHeight || 1080
  const maxT = points.length > 0 ? points[points.length - 1].t : 100

  const vertexData = points.map(p => ({
    x: (p.x / fw) * 2 - 1,
    y: -((p.y / fh) * 2 - 1),
    z: p.z * 2 - 1,
    t: (p.t / maxT) * 2 - 1
  }))

  const rect = container.value.getBoundingClientRect()
  const w = rect.width || 600, h = rect.height || 400

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

  camera = new THREE.PerspectiveCamera(50, w / h, 0.1, 10)
  camera.position.set(2.5, 1.8, 3)
  camera.lookAt(0, 0, 0)

  renderer = new THREE.WebGLRenderer({ antialias: true })
  renderer.setSize(w, h)
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
  container.value.appendChild(renderer.domElement)

  controls = new OrbitControls(camera, renderer.domElement)
  controls.enableDamping = true
  controls.dampingFactor = 0.08
  controls.target.set(0, 0, 0)
  controls.update()

  // ---- Axes helper with labels ----
  const axesLen = 1.2
  const axesMat = (color: number) => new THREE.LineBasicMaterial({ color })

  // X axis (red) — screen x
  const xLine = new THREE.Line(
    new THREE.BufferGeometry().setFromPoints([
      new THREE.Vector3(-axesLen, -axesLen, -axesLen),
      new THREE.Vector3(axesLen, -axesLen, -axesLen)
    ]),
    axesMat(0xff4444)
  )
  scene.add(xLine)
  objects.push(xLine)

  // Y axis (green) — screen y
  const yLine = new THREE.Line(
    new THREE.BufferGeometry().setFromPoints([
      new THREE.Vector3(-axesLen, -axesLen, -axesLen),
      new THREE.Vector3(-axesLen, axesLen, -axesLen)
    ]),
    axesMat(0x44ff44)
  )
  scene.add(yLine)
  objects.push(yLine)

  // Z axis (blue) — depth
  const zLine = new THREE.Line(
    new THREE.BufferGeometry().setFromPoints([
      new THREE.Vector3(-axesLen, -axesLen, -axesLen),
      new THREE.Vector3(-axesLen, -axesLen, axesLen)
    ]),
    axesMat(0x4488ff)
  )
  scene.add(zLine)
  objects.push(zLine)

  // T axis (yellow) — time (at an angle for 3D effect)
  const tLine = new THREE.Line(
    new THREE.BufferGeometry().setFromPoints([
      new THREE.Vector3(-axesLen, -axesLen, -axesLen),
      new THREE.Vector3(axesLen, axesLen, axesLen)
    ]),
    axesMat(0xffdd44)
  )
  scene.add(tLine)
  objects.push(tLine)

  // ---- Cube wireframe ----
  const cubeSize = axesLen * 2
  const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize)
  const cubeWire = new THREE.LineSegments(
    new THREE.EdgesGeometry(cubeGeo),
    new THREE.LineBasicMaterial({ color: 0x444466, transparent: true, opacity: 0.3 })
  )
  cubeWire.position.set(0, 0, 0)
  scene.add(cubeWire)
  objects.push(cubeWire)

  // ---- Points: color by time (t) ----
  if (vertexData.length > 0) {
    const positions = new Float32Array(vertexData.length * 3)
    const colors = new Float32Array(vertexData.length * 3)
    const color = new THREE.Color()

    for (let i = 0; i < vertexData.length; i++) {
      const p = vertexData[i]
      // Position: (x, y, z) with time as movement along diagonal
      positions[i * 3] = p.x
      positions[i * 3 + 1] = p.y
      positions[i * 3 + 2] = p.t * 0.5  // compress time a bit

      // Color gradient: blue (early) → cyan → yellow → red (late)
      const tNorm = (p.t + 1) / 2  // 0..1
      color.setHSL(0.6 - tNorm * 0.6, 0.9, 0.5)
      colors[i * 3] = color.r
      colors[i * 3 + 1] = color.g
      colors[i * 3 + 2] = color.b
    }

    const ptGeo = new THREE.BufferGeometry()
    ptGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3))
    ptGeo.setAttribute('color', new THREE.BufferAttribute(colors, 3))

    const ptMat = new THREE.PointsMaterial({
      size: 0.03,
      vertexColors: true,
      transparent: true,
      opacity: 0.8,
      sizeAttenuation: true
    })
    const pointsObj = new THREE.Points(ptGeo, ptMat)
    scene.add(pointsObj)
    objects.push(pointsObj)

    // ---- Trajectory line ----
    const linePositions = new Float32Array(vertexData.length * 3)
    for (let i = 0; i < vertexData.length; i++) {
      const p = vertexData[i]
      linePositions[i * 3] = p.x
      linePositions[i * 3 + 1] = p.y
      linePositions[i * 3 + 2] = p.t * 0.5
    }
    const lineGeo = new THREE.BufferGeometry()
    lineGeo.setAttribute('position', new THREE.BufferAttribute(linePositions, 3))
    const lineMat = new THREE.LineBasicMaterial({
      color: 0x88ccff,
      transparent: true,
      opacity: 0.35
    })
    const line = new THREE.Line(lineGeo, lineMat)
    scene.add(line)
    objects.push(line)
  }

  // ---- Lights ----
  const ambient = new THREE.AmbientLight(0x404060)
  scene.add(ambient)
  const dir = new THREE.DirectionalLight(0xffffff, 0.8)
  dir.position.set(1, 2, 1)
  scene.add(dir)

  // ---- Grid helper (subtle) ----
  const gridHelper = new THREE.GridHelper(2.5, 10, 0x444466, 0x333355)
  gridHelper.position.y = -axesLen - 0.05
  scene.add(gridHelper)
  objects.push(gridHelper)

  // Resize
  const resizeObserver = new ResizeObserver(() => {
    if (!container.value || !renderer || !camera) return
    const r = container.value.getBoundingClientRect()
    const rw = r.width || 600, rh = r.height || 400
    renderer.setSize(rw, rh)
    camera.aspect = rw / rh
    camera.updateProjectionMatrix()
  })
  resizeObserver.observe(container.value)
  ;(container.value as any).__resizeObserver = resizeObserver

  animate()

  // Notify demo runner via callback URL if present
  const cb = new URLSearchParams(window.location.search).get("_callback")
  if (cb) {
    fetch(cb, { mode: "no-cors" }).catch(() => {})
  }
}

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

onMounted(() => {
  tryAutoLoad()
})

watch(() => props.traces, () => {
  tryAutoLoad()
}, { deep: false })

function tryAutoLoad() {
  if (props.traces?.length && !selectedTraceId.value && !loading.value) {
    selectedTraceId.value = props.traces[0].trace_id
    loadData()
  }
}

onBeforeUnmount(() => {
  cancelAnimationFrame(animId)
  if ((container.value as any)?.__resizeObserver) {
    (container.value as any).__resizeObserver.disconnect()
  }
  disposeScene()
})
</script>