/**
 * Scene — the 3D Three.js rendering of the universe.
 *
 *  - Builds body meshes, orbit lines, vessel markers
 *  - Adds atmospheric glow on planets
 *  - Uses CameraController for log-scale distance, mouse zoom,
 *    drag-to-rotate, click-to-track via raycasting
 */
import { useEffect, useRef } from 'react';
import * as THREE from 'three';
import { sampleOrbit } from '@kerbal-rt/orbital-math';
import type { UniverseSnapshot } from '@kerbal-rt/shared-types';
import { bodyColor, vesselColor } from './color.js';
import { bodyPositionAt, vesselPositionAt } from './layout.js';
import { CameraController } from './camera.js';
import { createGlow } from './glow.js';

export interface SceneProps {
  snapshot: UniverseSnapshot;
  ut: number;
  followId: string | null;
  showPlanetOrbits: boolean;
  showMoonOrbits: boolean;
  showVesselOrbits: boolean;
  onSelect: (id: string | null) => void;
}

interface SceneRefs {
  scene: THREE.Scene;
  camera: THREE.PerspectiveCamera;
  renderer: THREE.WebGLRenderer;
  bodyMeshes: Map<string, THREE.Mesh>;
  vesselMeshes: Map<string, THREE.Mesh>;
  orbitLines: Map<string, THREE.Line>;
  controller: CameraController;
  raf: number;
}

const ORBIT_OPACITY: Record<string, number> = {
  planet: 0.5,
  moon: 0.4,
  vessel: 0.6,
};

export function Scene(props: SceneProps) {
  const { snapshot, ut, followId, showPlanetOrbits, showMoonOrbits, showVesselOrbits, onSelect } =
    props;
  const mountRef = useRef<HTMLDivElement>(null);
  const refsRef = useRef<SceneRefs | null>(null);
  // Latest-snapshot / ut / followId refs so the camera controller
  // always sees the current values without needing to be recreated.
  const stateRef = useRef({ snapshot, ut, followId, onSelect });
  stateRef.current = { snapshot, ut, followId, onSelect };

  // One-time scene setup
  useEffect(() => {
    const mount = mountRef.current;
    if (!mount) return;
    const refs = createScene(mount, stateRef);
    refsRef.current = refs;

    const onResize = () => {
      const r = refsRef.current;
      if (!r) return;
      const w = mount.clientWidth;
      const h = mount.clientHeight;
      r.camera.aspect = w / h;
      r.camera.updateProjectionMatrix();
      r.renderer.setSize(w, h);
    };
    window.addEventListener('resize', onResize);

    return () => {
      window.removeEventListener('resize', onResize);
      cancelAnimationFrame(refs.raf);
      refs.controller.dispose();
      refs.renderer.dispose();
      if (mount.contains(refs.renderer.domElement)) {
        mount.removeChild(refs.renderer.domElement);
      }
    };
  }, []);

  // Rebuild meshes when bodies/vessels set changes
  useEffect(() => {
    const refs = refsRef.current;
    if (!refs) return;
    rebuildMeshes(refs, snapshot);
  }, [snapshot.bodies, snapshot.vessels, snapshot]);

  // Toggle orbit visibility
  useEffect(() => {
    const refs = refsRef.current;
    if (!refs) return;
    for (const [id, line] of refs.orbitLines.entries()) {
      const isPlanet = snapshot.bodies.find((b) => b.id === id && b.kind === 'planet');
      const isMoon = snapshot.bodies.find((b) => b.id === id && b.kind === 'moon');
      if (isPlanet) line.visible = showPlanetOrbits;
      else if (isMoon) line.visible = showMoonOrbits;
      else line.visible = showVesselOrbits;
    }
  }, [showPlanetOrbits, showMoonOrbits, showVesselOrbits, snapshot.bodies]);

  // Per-frame
  useEffect(() => {
    const refs = refsRef.current;
    if (!refs) return;

    let lastUt = Number.NEGATIVE_INFINITY;
    const render = () => {
      const cur = stateRef.current;
      if (cur.ut !== lastUt) {
        lastUt = cur.ut;
        positionMeshes(refs, cur.snapshot, cur.ut);
      }
      refs.controller.update();
      refs.renderer.render(refs.scene, refs.camera);
      refs.raf = requestAnimationFrame(render);
    };
    refs.raf = requestAnimationFrame(render);
    return () => cancelAnimationFrame(refs.raf);
  }, []);

  return <div ref={mountRef} style={{ width: '100%', height: '100%', cursor: 'grab' }} />;
}

// ─── Three.js setup helpers ────────────────────────────────────────────────

function createScene(
  mount: HTMLDivElement,
  stateRef: React.MutableRefObject<{
    snapshot: UniverseSnapshot;
    ut: number;
    followId: string | null;
    onSelect: (id: string | null) => void;
  }>,
): SceneRefs {
  const width = mount.clientWidth;
  const height = mount.clientHeight;

  const scene = new THREE.Scene();
  scene.background = new THREE.Color(0x000005);

  const camera = new THREE.PerspectiveCamera(60, width / height, 1e6, 1e12);
  camera.position.set(0, 5e9, 1.5e10);
  camera.lookAt(0, 0, 0);

  const renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(width, height);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  mount.appendChild(renderer.domElement);

  scene.add(new THREE.AmbientLight(0x404040, 0.4));
  scene.add(new THREE.PointLight(0xffffff, 2, 0, 0));

  const controller = new CameraController({
    camera,
    domElement: mount,
    getSnapshot: () => stateRef.current.snapshot,
    getUt: () => stateRef.current.ut,
    getFollowId: () => stateRef.current.followId,
    onSelect: (id) => stateRef.current.onSelect(id),
  });

  return {
    scene,
    camera,
    renderer,
    bodyMeshes: new Map(),
    vesselMeshes: new Map(),
    orbitLines: new Map(),
    controller,
    raf: 0,
  };
}

function rebuildMeshes(refs: SceneRefs, snap: UniverseSnapshot): void {
  for (const mesh of refs.bodyMeshes.values()) {
    refs.scene.remove(mesh);
    mesh.geometry.dispose();
    if (Array.isArray(mesh.material)) {
      mesh.material.forEach((m) => m.dispose());
    } else {
      (mesh.material as THREE.Material).dispose();
    }
  }
  for (const mesh of refs.vesselMeshes.values()) {
    refs.scene.remove(mesh);
    mesh.geometry.dispose();
    (mesh.material as THREE.Material).dispose();
  }
  for (const line of refs.orbitLines.values()) {
    refs.scene.remove(line);
    line.geometry.dispose();
    (line.material as THREE.Material).dispose();
  }
  refs.bodyMeshes.clear();
  refs.vesselMeshes.clear();
  refs.orbitLines.clear();

  for (const body of snap.bodies) {
    if (body.kind === 'star') {
      const geo = new THREE.SphereGeometry(Math.max(body.radius, 1e8), 32, 16);
      const mat = new THREE.MeshBasicMaterial({ color: bodyColor(body.id) });
      const mesh = new THREE.Mesh(geo, mat);
      mesh.userData = { id: body.id };
      refs.scene.add(mesh);
      refs.bodyMeshes.set(body.id, mesh);
      continue;
    }
    if (body.parentId === null) continue;

    const displayRadius = Math.max(body.radius, 1e6);
    const geo = new THREE.SphereGeometry(displayRadius, 32, 16);
    const mat = new THREE.MeshPhongMaterial({
      color: bodyColor(body.id),
      emissive: 0x111111,
    });
    const mesh = new THREE.Mesh(geo, mat);
    mesh.userData = { id: body.id };
    refs.scene.add(mesh);
    refs.bodyMeshes.set(body.id, mesh);

    // Atmospheric glow for planets/moons (skip very small bodies)
    if (body.kind === 'planet' || (body.kind === 'moon' && body.radius > 100_000)) {
      const glow = createGlow(body.radius, bodyColor(body.id), 0.35, 1.35);
      mesh.add(glow); // attach as child so it follows position
    }

    // Orbit line
    const points = sampleOrbit(body.orbit, body.gravitationalParameter, 256);
    const positions = new Float32Array(points.length * 3);
    points.forEach((p, i) => {
      positions[i * 3] = p.x;
      positions[i * 3 + 1] = p.y;
      positions[i * 3 + 2] = p.z;
    });
    const lineGeo = new THREE.BufferGeometry();
    lineGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
    const lineMat = new THREE.LineBasicMaterial({
      color: bodyColor(body.id),
      opacity: ORBIT_OPACITY[body.kind] ?? 0.5,
      transparent: true,
    });
    const line = new THREE.LineLoop(lineGeo, lineMat);
    refs.scene.add(line);
    refs.orbitLines.set(body.id, line);
  }

  for (const vessel of snap.vessels) {
    const geo = new THREE.SphereGeometry(2e5, 12, 8);
    const mat = new THREE.MeshBasicMaterial({ color: vesselColor(vessel.owner) });
    const mesh = new THREE.Mesh(geo, mat);
    mesh.userData = { id: vessel.id };
    refs.scene.add(mesh);
    refs.vesselMeshes.set(vessel.id, mesh);

    if (vessel.referenceBodyId) {
      const ref = snap.bodies.find((b) => b.id === vessel.referenceBodyId);
      if (ref) {
        const points = sampleOrbit(vessel.orbit, ref.gravitationalParameter, 128);
        const positions = new Float32Array(points.length * 3);
        points.forEach((p, i) => {
          positions[i * 3] = p.x;
          positions[i * 3 + 1] = p.y;
          positions[i * 3 + 2] = p.z;
        });
        const lineGeo = new THREE.BufferGeometry();
        lineGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
        const lineMat = new THREE.LineBasicMaterial({
          color: vesselColor(vessel.owner),
          opacity: 0.5,
          transparent: true,
        });
        const line = new THREE.LineLoop(lineGeo, lineMat);
        refs.scene.add(line);
        refs.orbitLines.set(vessel.id, line);
      }
    }
  }
}

function positionMeshes(refs: SceneRefs, snap: UniverseSnapshot, ut: number): void {
  for (const body of snap.bodies) {
    if (body.kind === 'star' || body.parentId === null) continue;
    const pos = bodyPositionAt(snap.bodies, body.id, ut);
    const mesh = refs.bodyMeshes.get(body.id);
    if (mesh) mesh.position.set(pos.x, pos.y, pos.z);
  }
  for (const vessel of snap.vessels) {
    const pos = vesselPositionAt(snap.bodies, vessel, ut);
    const mesh = refs.vesselMeshes.get(vessel.id);
    if (mesh) mesh.position.set(pos.x, pos.y, pos.z);
  }
}
