'use client';

import { useEffect, useRef } from 'react';
import * as THREE from 'three';

export type StackNode = { label: string; color: string };

/**
 * An interactive 3D constellation of the tech stack. Every tool is a glowing
 * dot positioned on a Fibonacci sphere and tinted by its category color. The
 * globe auto-rotates, can be dragged to spin, and reveals a tooltip with the
 * tool name when a dot is hovered (raycast). Everything is torn down on unmount
 * — RAF, GL context, geometries, materials, textures, and listeners.
 */
export function StackCanvas({ nodes }: { nodes: StackNode[] }) {
  const mountRef = useRef<HTMLDivElement>(null);
  const tooltipRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const mount = mountRef.current;
    const tooltip = tooltipRef.current;
    if (!mount || !tooltip || nodes.length === 0) return;

    const prefersReduced = window.matchMedia(
      '(prefers-reduced-motion: reduce)',
    ).matches;

    // --- Sizing -------------------------------------------------------------
    let width = mount.clientWidth || 600;
    let height = mount.clientHeight || 460;

    // --- Renderer -----------------------------------------------------------
    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.setSize(width, height);
    renderer.setClearColor(0x000000, 0);
    mount.appendChild(renderer.domElement);
    renderer.domElement.style.touchAction = 'pan-y';
    renderer.domElement.style.cursor = 'grab';

    // --- Scene / camera -----------------------------------------------------
    const scene = new THREE.Scene();
    const R = 2.6;
    const dist = 6.6;
    const camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 100);
    camera.position.set(0, 0, dist);

    const group = new THREE.Group();
    scene.add(group);

    // --- Wireframe backdrop globe ------------------------------------------
    const wireGeo = new THREE.IcosahedronGeometry(R, 2);
    const wire = new THREE.LineSegments(
      new THREE.WireframeGeometry(wireGeo),
      new THREE.LineBasicMaterial({
        color: 0x38bdf8,
        transparent: true,
        opacity: 0.08,
      }),
    );
    wireGeo.dispose();
    group.add(wire);

    // --- Glow sprite texture (shared) --------------------------------------
    const glowCanvas = document.createElement('canvas');
    glowCanvas.width = glowCanvas.height = 64;
    const gctx = glowCanvas.getContext('2d')!;
    const grad = gctx.createRadialGradient(32, 32, 0, 32, 32, 32);
    grad.addColorStop(0, 'rgba(255,255,255,1)');
    grad.addColorStop(0.25, 'rgba(255,255,255,0.85)');
    grad.addColorStop(1, 'rgba(255,255,255,0)');
    gctx.fillStyle = grad;
    gctx.fillRect(0, 0, 64, 64);
    const glowTex = new THREE.CanvasTexture(glowCanvas);

    // --- Nodes as sprites on a Fibonacci sphere ----------------------------
    const golden = Math.PI * (3 - Math.sqrt(5));
    const sprites: THREE.Sprite[] = [];
    const materials: THREE.SpriteMaterial[] = [];
    const n = nodes.length;

    nodes.forEach((node, i) => {
      const y = 1 - (i / Math.max(1, n - 1)) * 2;
      const r = Math.sqrt(Math.max(0, 1 - y * y));
      const theta = i * golden;
      const pos = new THREE.Vector3(
        Math.cos(theta) * r,
        y,
        Math.sin(theta) * r,
      ).multiplyScalar(R);

      const mat = new THREE.SpriteMaterial({
        map: glowTex,
        color: new THREE.Color(node.color),
        transparent: true,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
      });
      const sprite = new THREE.Sprite(mat);
      sprite.position.copy(pos);
      sprite.scale.setScalar(0.5);
      sprite.userData = { label: node.label, color: node.color, base: 0.5 };
      group.add(sprite);
      sprites.push(sprite);
      materials.push(mat);
    });

    // --- Interaction state --------------------------------------------------
    let dragging = false;
    let lastX = 0;
    let lastY = 0;
    let velX = 0;
    let velY = 0;
    const auto = prefersReduced ? 0 : 0.0018;
    let hovered: THREE.Sprite | null = null;

    const raycaster = new THREE.Raycaster();
    const pointer = new THREE.Vector2();
    let pointerInside = false;

    const onPointerDown = (e: PointerEvent) => {
      dragging = true;
      lastX = e.clientX;
      lastY = e.clientY;
      renderer.domElement.setPointerCapture(e.pointerId);
      renderer.domElement.style.cursor = 'grabbing';
    };
    const onPointerMove = (e: PointerEvent) => {
      const rect = renderer.domElement.getBoundingClientRect();
      pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
      pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
      pointerInside = true;
      if (dragging) {
        const dx = e.clientX - lastX;
        const dy = e.clientY - lastY;
        lastX = e.clientX;
        lastY = e.clientY;
        velY = dx * 0.005;
        velX = dy * 0.005;
      }
    };
    const onPointerUp = (e: PointerEvent) => {
      dragging = false;
      try {
        renderer.domElement.releasePointerCapture(e.pointerId);
      } catch {
        /* noop */
      }
      renderer.domElement.style.cursor = 'grab';
    };
    const onPointerLeave = () => {
      pointerInside = false;
    };

    renderer.domElement.addEventListener('pointerdown', onPointerDown);
    renderer.domElement.addEventListener('pointermove', onPointerMove);
    window.addEventListener('pointerup', onPointerUp);
    renderer.domElement.addEventListener('pointerleave', onPointerLeave);

    // --- Resize -------------------------------------------------------------
    const ro = new ResizeObserver(() => {
      width = mount.clientWidth || width;
      height = mount.clientHeight || height;
      camera.aspect = width / height;
      camera.updateProjectionMatrix();
      renderer.setSize(width, height);
    });
    ro.observe(mount);

    // --- Render loop --------------------------------------------------------
    let raf = 0;
    const tmp = new THREE.Vector3();

    const tick = () => {
      raf = requestAnimationFrame(tick);

      // Rotation: apply velocity + gentle auto-spin, with decay when idle.
      if (!dragging) {
        velY *= 0.94;
        velX *= 0.94;
      }
      group.rotation.y += velY + auto;
      group.rotation.x += velX;
      group.rotation.x = Math.max(-0.6, Math.min(0.6, group.rotation.x));

      group.updateMatrixWorld();

      // Hover raycast (only when not dragging and pointer is inside).
      if (pointerInside && !dragging) {
        raycaster.setFromCamera(pointer, camera);
        const hits = raycaster.intersectObjects(sprites, false);
        const next = (hits[0]?.object as THREE.Sprite) ?? null;
        if (next !== hovered) {
          hovered = next;
        }
      } else if (!pointerInside) {
        hovered = null;
      }

      // Scale + tooltip for the hovered sprite.
      for (const s of sprites) {
        const target = s === hovered ? 0.85 : 0.5;
        const cur = s.scale.x;
        s.scale.setScalar(cur + (target - cur) * 0.2);
      }

      if (hovered) {
        hovered.getWorldPosition(tmp);
        tmp.project(camera);
        const sx = (tmp.x * 0.5 + 0.5) * width;
        const sy = (-tmp.y * 0.5 + 0.5) * height;
        const data = hovered.userData as { label: string; color: string };
        tooltip.textContent = data.label;
        tooltip.style.transform = `translate(${sx}px, ${sy}px) translate(-50%, -160%)`;
        tooltip.style.borderColor = data.color;
        tooltip.style.color = data.color;
        tooltip.style.opacity = '1';
        renderer.domElement.style.cursor = 'pointer';
      } else {
        tooltip.style.opacity = '0';
        if (!dragging) renderer.domElement.style.cursor = 'grab';
      }

      renderer.render(scene, camera);
    };
    tick();

    // --- Teardown -----------------------------------------------------------
    return () => {
      cancelAnimationFrame(raf);
      ro.disconnect();
      renderer.domElement.removeEventListener('pointerdown', onPointerDown);
      renderer.domElement.removeEventListener('pointermove', onPointerMove);
      window.removeEventListener('pointerup', onPointerUp);
      renderer.domElement.removeEventListener('pointerleave', onPointerLeave);
      materials.forEach((m) => m.dispose());
      glowTex.dispose();
      wire.geometry.dispose();
      (wire.material as THREE.Material).dispose();
      renderer.dispose();
      if (renderer.domElement.parentNode === mount) {
        mount.removeChild(renderer.domElement);
      }
    };
  }, [nodes]);

  return (
    <div
      ref={mountRef}
      className="relative h-[400px] w-full select-none sm:h-[460px] lg:h-[520px]"
    >
      <div
        ref={tooltipRef}
        className="pointer-events-none absolute left-0 top-0 z-10 whitespace-nowrap rounded-full border bg-base-900/80 px-3 py-1 font-mono text-[0.72rem] tracking-wide backdrop-blur transition-opacity duration-150"
        style={{ opacity: 0 }}
      />
    </div>
  );
}
