import { useEffect, useRef, useState } from "react";
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";

export type LidarGroundViewMode =
  | "intensity"
  | "current"
  | "candidate"
  | "disagreement"
  | "candidate-disagreement"
  | "semantic"
  | "ground-truth";

export interface LidarGroundPointCloudFrame {
  pointCount: number;
  pointsXyzM: Array<[number, number, number]>;
  intensity0To255: number[] | null;
  semanticRgb0To255?: number[] | null;
  masks: {
    currentGround: number[];
    currentAssigned: number[];
    candidateGround: number[];
    candidateAssigned: number[];
    disagreement: number[];
    candidateDisagreement?: number[];
    groundTruthGround?: number[];
    evaluationMask?: number[];
  };
}

interface LidarGroundPointCloudProps {
  frame: LidarGroundPointCloudFrame;
  mode: LidarGroundViewMode;
  coordinateFrame?: "auto-fit" | "sensor-fixed";
  sensorHeightM?: number;
}

function percentile(values: number[], fraction: number): number {
  if (!values.length) return 0;
  const sorted = [...values].sort((left, right) => left - right);
  const index = Math.min(
    sorted.length - 1,
    Math.max(0, Math.floor((sorted.length - 1) * fraction)),
  );
  return sorted[index];
}

function setRgb(
  target: Float32Array,
  offset: number,
  red: number,
  green: number,
  blue: number,
) {
  target[offset] = red;
  target[offset + 1] = green;
  target[offset + 2] = blue;
}

function frameColors(
  frame: LidarGroundPointCloudFrame,
  mode: LidarGroundViewMode,
): Float32Array {
  const colors = new Float32Array(frame.pointCount * 3);
  for (let index = 0; index < frame.pointCount; index += 1) {
    const offset = index * 3;
    const current = frame.masks.currentGround[index] === 1;
    const candidate = frame.masks.candidateGround[index] === 1;
    const candidateAssigned = frame.masks.candidateAssigned[index] === 1;
    if (mode === "intensity") {
      const intensity = (frame.intensity0To255?.[index] ?? 96) / 255;
      const neutral = 0.16 + intensity * 0.8;
      setRgb(
        colors,
        offset,
        neutral,
        neutral,
        neutral,
      );
    } else if (mode === "semantic") {
      const semantic = frame.semanticRgb0To255;
      setRgb(
        colors,
        offset,
        (semantic?.[offset] ?? 96) / 255,
        (semantic?.[offset + 1] ?? 96) / 255,
        (semantic?.[offset + 2] ?? 96) / 255,
      );
    } else if (mode === "ground-truth") {
      const groundTruth = frame.masks.groundTruthGround?.[index] === 1;
      const evaluated = frame.masks.evaluationMask?.[index] !== 0;
      if (!evaluated) {
        setRgb(colors, offset, 0.86, 0.61, 0.24);
      } else {
        setRgb(
          colors,
          offset,
          groundTruth ? 0.76 : 0.29,
          groundTruth ? 0.86 : 0.33,
          groundTruth ? 0.52 : 0.36,
        );
      }
    } else if (mode === "current") {
      setRgb(
        colors,
        offset,
        current ? 0.88 : 0.29,
        current ? 0.68 : 0.34,
        current ? 0.28 : 0.37,
      );
    } else if (mode === "candidate") {
      if (!candidateAssigned) {
        setRgb(colors, offset, 1, 0.24, 0.32);
      } else {
        setRgb(
          colors,
          offset,
          candidate ? 0.78 : 0.29,
          candidate ? 0.91 : 0.34,
          candidate ? 0.48 : 0.37,
        );
      }
    } else if (mode === "disagreement") {
      const disagreement = frame.masks.disagreement[index] === 1;
      setRgb(
        colors,
        offset,
        disagreement ? 1 : 0.24,
        disagreement ? 0.31 : 0.29,
        disagreement ? 0.22 : 0.35,
      );
    } else if (mode === "candidate-disagreement") {
      const disagreement = frame.masks.candidateDisagreement?.[index] === 1;
      setRgb(
        colors,
        offset,
        disagreement ? 1 : 0.24,
        disagreement ? 0.31 : 0.29,
        disagreement ? 0.22 : 0.35,
      );
    } else {
      setRgb(colors, offset, candidate ? 0.78 : 0.29, candidate ? 0.91 : 0.34, 0.4);
    }
  }
  return colors;
}

export function LidarGroundPointCloud({
  frame,
  mode,
  coordinateFrame = "auto-fit",
  sensorHeightM = 0,
}: LidarGroundPointCloudProps) {
  const hostRef = useRef<HTMLDivElement | null>(null);
  const geometryRef = useRef<THREE.BufferGeometry | null>(null);
  const materialRef = useRef<THREE.PointsMaterial | null>(null);
  const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
  const controlsRef = useRef<OrbitControls | null>(null);
  const fogRef = useRef<THREE.FogExp2 | null>(null);
  const gridRef = useRef<THREE.GridHelper | null>(null);
  const viewRadiusRef = useRef(1);
  const viewTargetHeightRef = useRef(0.25);
  const viewInitializedRef = useRef(false);
  const [renderError, setRenderError] = useState<string | null>(null);

  useEffect(() => {
    const host = hostRef.current;
    if (!host) return;

    let renderer: THREE.WebGLRenderer;
    try {
      renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: true,
        powerPreference: "high-performance",
      });
    } catch {
      setRenderError("Браузер не смог создать WebGL-сцену LiDAR.");
      return;
    }
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.outputColorSpace = THREE.SRGBColorSpace;
    renderer.setClearColor(0x06070a, 1);
    renderer.domElement.setAttribute(
      "aria-label",
      "Интерактивное облако LiDAR",
    );
    host.prepend(renderer.domElement);

    const scene = new THREE.Scene();
    const fog = new THREE.FogExp2(0x06070a, 0.035);
    scene.fog = fog;
    fogRef.current = fog;
    const camera = new THREE.PerspectiveCamera(48, 1, 0.01, 1_000);
    camera.position.set(6, 4.5, 6);
    cameraRef.current = camera;

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.08;
    controls.enablePan = true;
    controls.enableZoom = true;
    controls.minDistance = 0.15;
    controls.maxDistance = 200;
    controls.minPolarAngle = 0;
    controls.maxPolarAngle = Math.PI;
    controls.target.set(0, 0.5, 0);
    controls.update();
    controlsRef.current = controls;

    const geometry = new THREE.BufferGeometry();
    geometryRef.current = geometry;
    const material = new THREE.PointsMaterial({
      size: 0.035,
      sizeAttenuation: true,
      vertexColors: true,
      transparent: true,
      opacity: 0.96,
      depthWrite: true,
    });
    materialRef.current = material;
    scene.add(new THREE.Points(geometry, material));

    const grid = new THREE.GridHelper(10, 40, 0x454846, 0x252725);
    gridRef.current = grid;
    const gridMaterials = Array.isArray(grid.material)
      ? grid.material
      : [grid.material];
    gridMaterials.forEach((gridMaterial) => {
      gridMaterial.transparent = true;
      gridMaterial.opacity = 0.3;
    });
    scene.add(grid);

    const axes = new THREE.AxesHelper(0.8);
    axes.position.set(-0.05, 0.02, -0.05);
    scene.add(axes);

    const resize = () => {
      const width = Math.max(host.clientWidth, 1);
      const height = Math.max(host.clientHeight, 1);
      camera.aspect = width / height;
      camera.updateProjectionMatrix();
      renderer.setSize(width, height, false);
    };
    const observer = new ResizeObserver(resize);
    observer.observe(host);
    resize();

    let animationFrame = 0;
    const render = () => {
      animationFrame = window.requestAnimationFrame(render);
      controls.update();
      renderer.render(scene, camera);
    };
    render();

    return () => {
      window.cancelAnimationFrame(animationFrame);
      observer.disconnect();
      controls.dispose();
      geometry.dispose();
      material.dispose();
      grid.geometry.dispose();
      gridMaterials.forEach((gridMaterial) => gridMaterial.dispose());
      axes.geometry.dispose();
      const axesMaterials = Array.isArray(axes.material)
        ? axes.material
        : [axes.material];
      axesMaterials.forEach((axesMaterial) => axesMaterial.dispose());
      renderer.dispose();
      renderer.domElement.remove();
      geometryRef.current = null;
      materialRef.current = null;
      cameraRef.current = null;
      controlsRef.current = null;
      fogRef.current = null;
      gridRef.current = null;
      viewRadiusRef.current = 1;
      viewTargetHeightRef.current = 0.25;
      viewInitializedRef.current = false;
    };
  }, []);

  useEffect(() => {
    const geometry = geometryRef.current;
    const material = materialRef.current;
    const camera = cameraRef.current;
    const controls = controlsRef.current;
    const fog = fogRef.current;
    const grid = gridRef.current;
    if (!geometry || !material || !camera || !controls || !fog || !grid) return;

    const positions = new Float32Array(frame.pointCount * 3);
    const sampleStep = Math.max(1, Math.floor(frame.pointCount / 5_000));
    const sampleX: number[] = [];
    const sampleY: number[] = [];
    const sampleZ: number[] = [];
    frame.pointsXyzM.forEach(([x, y, z], index) => {
      if (index % sampleStep !== 0) return;
      sampleX.push(x);
      sampleY.push(y);
      sampleZ.push(z);
    });
    const minimumX = percentile(sampleX, 0.01);
    const maximumX = percentile(sampleX, 0.99);
    const minimumY = percentile(sampleY, 0.01);
    const maximumY = percentile(sampleY, 0.99);
    const minimumZ = percentile(sampleZ, 0.01);
    const maximumZ = percentile(sampleZ, 0.99);
    const centerX = coordinateFrame === "sensor-fixed"
      ? 0
      : (minimumX + maximumX) / 2;
    const centerY = coordinateFrame === "sensor-fixed"
      ? 0
      : (minimumY + maximumY) / 2;
    frame.pointsXyzM.forEach(([x, y, z], index) => {
      const offset = index * 3;
      positions[offset] = x - centerX;
      positions[offset + 1] = coordinateFrame === "sensor-fixed"
        ? z + sensorHeightM
        : z - minimumZ;
      positions[offset + 2] = -(y - centerY);
    });
    geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
    geometry.computeBoundingSphere();
    const radius = Math.max(
      Math.hypot(
        (maximumX - minimumX) / 2,
        (maximumY - minimumY) / 2,
        maximumZ - minimumZ,
      ),
      0.2,
    );
    viewRadiusRef.current = radius;
    material.size = THREE.MathUtils.clamp(radius / 155, 0.014, 0.075);
    fog.density = THREE.MathUtils.clamp(0.18 / radius, 0.0008, 0.035);
    grid.scale.setScalar(Math.max(radius / 5, 0.25));

    const targetHeight = coordinateFrame === "sensor-fixed"
      ? Math.max(sensorHeightM * 0.35, 0.35)
      : Math.max((maximumZ - minimumZ) * 0.42, 0.15);
    const distance = Math.max(radius * 1.15, 0.9);
    viewTargetHeightRef.current = targetHeight;
    camera.near = Math.max(distance / 1_000, 0.005);
    camera.far = Math.max(distance * 100, 100);
    camera.updateProjectionMatrix();
    if (coordinateFrame === "auto-fit" || !viewInitializedRef.current) {
      controls.target.set(0, targetHeight, 0);
      camera.position.set(distance, distance * 0.72, distance);
      viewInitializedRef.current = true;
    }
    controls.update();
  }, [coordinateFrame, frame, sensorHeightM]);

  useEffect(() => {
    const geometry = geometryRef.current;
    if (!geometry) return;
    geometry.setAttribute(
      "color",
      new THREE.BufferAttribute(frameColors(frame, mode), 3),
    );
    geometry.attributes.color.needsUpdate = true;
  }, [frame, mode]);

  const resetCamera = () => {
    const geometry = geometryRef.current;
    const camera = cameraRef.current;
    const controls = controlsRef.current;
    if (!geometry || !camera || !controls) return;
    const radius = Math.max(viewRadiusRef.current, 0.2);
    const distance = Math.max(radius * 1.15, 0.9);
    camera.position.set(distance, distance * 0.72, distance);
    controls.target.set(0, viewTargetHeightRef.current, 0);
    controls.update();
  };

  return (
    <div className="lidar-ground-scene" data-testid="lidar-ground-scene">
      <div ref={hostRef} className="lidar-ground-scene__viewport">
        {renderError ? (
          <p className="lidar-ground-scene__error">{renderError}</p>
        ) : null}
      </div>
      <div className="lidar-ground-scene__toolbar">
        <button type="button" onClick={resetCamera}>Сбросить ракурс</button>
        <span>ЛКМ · вращение</span>
        <span>Колесо · масштаб</span>
        <span>ПКМ · панорама</span>
      </div>
    </div>
  );
}
