<template>
  <div ref="host" class="pointcloud-viewer">
    <div class="pointcloud-toolbar">
      <span><i></i>{{ points.length }} 个抽样点</span>
      <el-radio-group v-model="colorMode" size="small" @change="renderPoints">
        <el-radio-button value="distance">距离</el-radio-button>
        <el-radio-button value="height">高程</el-radio-button>
        <el-radio-button value="intensity">强度</el-radio-button>
      </el-radio-group>
      <el-tooltip content="复位视角" placement="bottom">
        <el-button :icon="RefreshRight" circle size="small" @click="resetCamera" />
      </el-tooltip>
    </div>
    <div ref="canvasHost" class="pointcloud-canvas"></div>
    <div class="pointcloud-axis"><span class="axis-x">X</span><span class="axis-y">Y</span><span class="axis-z">Z</span></div>
  </div>
</template>

<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import { RefreshRight } from "@element-plus/icons-vue";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";

interface PointValue { x: number; y: number; z: number; value?: number }

const props = defineProps<{ points: PointValue[] }>();
const host = ref<HTMLElement>();
const canvasHost = ref<HTMLElement>();
const colorMode = ref("distance");
let renderer: THREE.WebGLRenderer | undefined;
let scene: THREE.Scene | undefined;
let camera: THREE.PerspectiveCamera | undefined;
let controls: OrbitControls | undefined;
let pointObject: THREE.Points | undefined;
let resizeObserver: ResizeObserver | undefined;
let animationFrame = 0;

function renderPoints() {
  if (!scene) return;
  if (pointObject) {
    scene.remove(pointObject);
    pointObject.geometry.dispose();
    (pointObject.material as THREE.Material).dispose();
  }
  const values = props.points.length ? props.points : fallbackPoints();
  const positions = new Float32Array(values.length * 3);
  const colors = new Float32Array(values.length * 3);
  values.forEach((point, index) => {
    positions[index * 3] = Number(point.x);
    positions[index * 3 + 1] = Number(point.z);
    positions[index * 3 + 2] = Number(point.y);
    const normalized = colorMode.value === "height"
      ? Math.min(1, Math.max(0, Number(point.z) / 4))
      : colorMode.value === "intensity"
        ? (index % 13) / 12
        : Number(point.value ?? (index % 10) / 10);
    const color = new THREE.Color();
    color.setHSL(0.58 - normalized * 0.48, 0.78, 0.48);
    colors[index * 3] = color.r;
    colors[index * 3 + 1] = color.g;
    colors[index * 3 + 2] = color.b;
  });
  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
  geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
  const material = new THREE.PointsMaterial({ size: 0.16, vertexColors: true, sizeAttenuation: true });
  pointObject = new THREE.Points(geometry, material);
  scene.add(pointObject);
}

function initialize() {
  if (!canvasHost.value) return;
  scene = new THREE.Scene();
  scene.background = new THREE.Color("#0b1726");
  camera = new THREE.PerspectiveCamera(48, 1, 0.1, 1000);
  renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.outputColorSpace = THREE.SRGBColorSpace;
  canvasHost.value.appendChild(renderer.domElement);
  controls = new OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;
  controls.dampingFactor = 0.08;
  controls.target.set(0, 1.2, 0);
  scene.add(new THREE.GridHelper(22, 22, 0x466a91, 0x1b334c));
  scene.add(new THREE.AxesHelper(2.5));
  const ambient = new THREE.AmbientLight(0xffffff, 0.7);
  scene.add(ambient);
  resetCamera();
  renderPoints();
  resizeObserver = new ResizeObserver(resize);
  resizeObserver.observe(canvasHost.value);
  animate();
}

function resize() {
  if (!renderer || !camera || !canvasHost.value) return;
  const width = Math.max(1, canvasHost.value.clientWidth);
  const height = Math.max(1, canvasHost.value.clientHeight);
  renderer.setSize(width, height, false);
  camera.aspect = width / height;
  camera.updateProjectionMatrix();
}

function resetCamera() {
  if (!camera || !controls) return;
  camera.position.set(10, 8, 12);
  controls.target.set(0, 1.2, 0);
  controls.update();
}

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

function fallbackPoints(): PointValue[] {
  return Array.from({ length: 80 }, (_, index) => {
    const angle = index * 0.42;
    const radius = 3 + (index % 12) * 0.25;
    return { x: Math.cos(angle) * radius, y: Math.sin(angle) * radius, z: (index % 16) * 0.18, value: (index % 10) / 10 };
  });
}

watch(() => props.points, renderPoints, { deep: true });
onMounted(initialize);
onBeforeUnmount(() => {
  cancelAnimationFrame(animationFrame);
  resizeObserver?.disconnect();
  controls?.dispose();
  pointObject?.geometry.dispose();
  if (pointObject) (pointObject.material as THREE.Material).dispose();
  renderer?.dispose();
  renderer?.domElement.remove();
});
</script>
