"use client";

import { useMemo, useRef } from "react";
import * as THREE from "three";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { Html, OrbitControls } from "@react-three/drei";
import { useReducedMotion } from "motion/react";
import {
  GLOBE_RADIUS,
  arcCurve,
  fibonacciSphere,
  graticuleGeometry,
  latLngToVector3,
  nodes,
  routes,
} from "./geo";
import {
  arcFragment,
  arcVertex,
  fresnelFragment,
  fresnelVertex,
  moteFragment,
  moteVertex,
} from "./shaders";

const COLORS = {
  cage: new THREE.Color("#3a56a6"),
  shell: new THREE.Color("#1b2a5b"),
  rim: new THREE.Color("#6f8ae0"),
  thread: new THREE.Color("#e0a458"),
  cotton: new THREE.Color("#f5f1e8"),
  prospect: new THREE.Color("#8a8f9c"),
};

/* -------------------------------------------------------------------------- */
/* Globe cage and shell                                                       */
/* -------------------------------------------------------------------------- */

function Graticule() {
  const geometry = useMemo(() => graticuleGeometry(13, 24, 128), []);

  return (
    <lineSegments geometry={geometry}>
      <lineBasicMaterial
        color={COLORS.cage}
        transparent
        opacity={0.28}
        depthWrite={false}
      />
    </lineSegments>
  );
}

function Shell() {
  const uniforms = useMemo(
    () => ({
      uColor: { value: COLORS.rim },
      uIntensity: { value: 0.85 },
      uPower: { value: 2.6 },
    }),
    [],
  );

  return (
    <>
      {/* Solid core, so arcs on the far side are correctly hidden. */}
      <mesh>
        <sphereGeometry args={[GLOBE_RADIUS * 0.995, 64, 64]} />
        <meshBasicMaterial color={COLORS.shell} transparent opacity={0.82} />
      </mesh>

      {/* Rim light. */}
      <mesh scale={1.035}>
        <sphereGeometry args={[GLOBE_RADIUS, 64, 64]} />
        <shaderMaterial
          vertexShader={fresnelVertex}
          fragmentShader={fresnelFragment}
          uniforms={uniforms}
          transparent
          blending={THREE.AdditiveBlending}
          side={THREE.BackSide}
          depthWrite={false}
        />
      </mesh>
    </>
  );
}

/** Evenly spread dots over the sphere, reading as the texture of the cloth. */
function SurfaceDots() {
  const positions = useMemo(
    () => fibonacciSphere(2200, GLOBE_RADIUS * 1.002),
    [],
  );

  const geometry = useMemo(() => {
    const g = new THREE.BufferGeometry();
    g.setAttribute("position", new THREE.BufferAttribute(positions, 3));
    return g;
  }, [positions]);

  return (
    <points geometry={geometry}>
      <pointsMaterial
        color={COLORS.cotton}
        size={0.008}
        sizeAttenuation
        transparent
        opacity={0.32}
        depthWrite={false}
      />
    </points>
  );
}

/* -------------------------------------------------------------------------- */
/* Trade routes                                                               */
/* -------------------------------------------------------------------------- */

function ThreadArc({
  from,
  to,
  kind,
  index,
  animate,
}: {
  from: THREE.Vector3;
  to: THREE.Vector3;
  kind: "market" | "prospect";
  index: number;
  animate: boolean;
}) {
  const material = useRef<THREE.ShaderMaterial>(null);

  const geometry = useMemo(() => {
    const curve = arcCurve(from, to, index);
    return new THREE.TubeGeometry(curve, 128, 0.009, 8, false);
  }, [from, to, index]);

  const uniforms = useMemo(
    () => ({
      uColor: {
        value: kind === "prospect" ? COLORS.prospect : COLORS.thread,
      },
      uBeadColor: { value: COLORS.cotton },
      uProgress: { value: animate ? 0 : 1 },
      uBead: { value: 0 },
      uBaseAlpha: { value: kind === "prospect" ? 0.35 : 0.8 },
      uBeadWidth: { value: 0.05 },
    }),
    [kind, animate],
  );

  useFrame(({ clock }) => {
    const mat = material.current;
    if (!mat || !animate) return;

    const t = clock.elapsedTime;

    // Each route stitches itself on, staggered so they arrive one after another.
    const start = 0.4 + index * 0.32;
    mat.uniforms.uProgress.value = THREE.MathUtils.clamp(
      (t - start) / 1.5,
      0,
      1,
    );

    // Then a bead of light runs the finished route on a loop.
    const speed = kind === "prospect" ? 0.11 : 0.16;
    mat.uniforms.uBead.value = (t * speed + index * 0.19) % 1;
  });

  return (
    <mesh geometry={geometry}>
      <shaderMaterial
        ref={material}
        vertexShader={arcVertex}
        fragmentShader={arcFragment}
        uniforms={uniforms}
        transparent
        blending={THREE.AdditiveBlending}
        depthWrite={false}
      />
    </mesh>
  );
}

/* -------------------------------------------------------------------------- */
/* Ports                                                                       */
/* -------------------------------------------------------------------------- */

function Marker({
  position,
  label,
  kind,
  animate,
  index,
}: {
  position: THREE.Vector3;
  label: string;
  kind: "origin" | "market" | "prospect";
  animate: boolean;
  index: number;
}) {
  const ring = useRef<THREE.Mesh>(null);
  const ringMaterial = useRef<THREE.MeshBasicMaterial>(null);
  const tag = useRef<HTMLSpanElement>(null);

  // Reused each frame so the label check allocates nothing.
  const worldPosition = useMemo(() => new THREE.Vector3(), []);
  const toCamera = useMemo(() => new THREE.Vector3(), []);

  const color =
    kind === "origin"
      ? COLORS.thread
      : kind === "prospect"
        ? COLORS.prospect
        : COLORS.cotton;

  // The ring lies flat against the surface at that point.
  const quaternion = useMemo(() => {
    const q = new THREE.Quaternion();
    q.setFromUnitVectors(new THREE.Vector3(0, 0, 1), position.clone().normalize());
    return q;
  }, [position]);

  useFrame(({ clock, camera }) => {
    if (ring.current && ringMaterial.current && animate) {
      const period = kind === "origin" ? 2.2 : 3.1;
      const t = ((clock.elapsedTime + index * 0.5) % period) / period;

      const scale = 1 + t * (kind === "origin" ? 4.5 : 3);
      ring.current.scale.setScalar(scale);
      ringMaterial.current.opacity = (1 - t) * (kind === "origin" ? 0.7 : 0.4);
    }

    // Labels live in the DOM, so they have no depth test of their own. Fade
    // them out as their port turns past the limb, otherwise every name on the
    // far side of the globe reads straight through it.
    if (tag.current && ring.current) {
      ring.current.getWorldPosition(worldPosition);
      toCamera.copy(camera.position).sub(worldPosition).normalize();

      const facing = worldPosition.clone().normalize().dot(toCamera);
      const opacity = THREE.MathUtils.smoothstep(facing, 0.05, 0.45);

      tag.current.style.opacity = String(opacity);
    }
  });

  return (
    <group position={position} quaternion={quaternion}>
      <mesh>
        <sphereGeometry args={[kind === "origin" ? 0.024 : 0.017, 16, 16]} />
        <meshBasicMaterial color={color} />
      </mesh>

      <mesh ref={ring} position={[0, 0, 0.002]}>
        <ringGeometry args={[0.028, 0.034, 40]} />
        <meshBasicMaterial
          ref={ringMaterial}
          color={color}
          transparent
          opacity={0.5}
          side={THREE.DoubleSide}
          depthWrite={false}
        />
      </mesh>

      <Html
        center={false}
        zIndexRange={[10, 0]}
        style={{ pointerEvents: "none", transform: "translate(14px, -50%)" }}
      >
        <span
          ref={tag}
          // Hidden on narrow screens, where the globe is atmosphere rather than
          // a map and the labels would land on top of the headline.
          className="hidden lg:block"
          style={{
            whiteSpace: "nowrap",
            fontFamily: "var(--font-mono)",
            fontSize: "10px",
            letterSpacing: "0.16em",
            textTransform: "uppercase",
            color: `#${color.getHexString()}`,
            opacity: 0,
            textShadow: "0 1px 6px rgba(8,11,20,0.9)",
          }}
        >
          {label}
        </span>
      </Html>
    </group>
  );
}

/* -------------------------------------------------------------------------- */
/* Yarn motes                                                                  */
/* -------------------------------------------------------------------------- */

function Motes({ count = 420, animate }: { count?: number; animate: boolean }) {
  const material = useRef<THREE.ShaderMaterial>(null);
  const { viewport } = useThree();

  const geometry = useMemo(() => {
    const positions = new Float32Array(count * 3);
    const scales = new Float32Array(count);
    const phases = new Float32Array(count);

    for (let i = 0; i < count; i++) {
      // Shell of motes around the globe, never inside it.
      const r = 1.6 + Math.random() * 2.4;
      const theta = Math.random() * Math.PI * 2;
      const phi = Math.acos(2 * Math.random() - 1);

      positions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
      positions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta) * 0.7;
      positions[i * 3 + 2] = r * Math.cos(phi);

      scales[i] = 0.4 + Math.random() * 1.6;
      phases[i] = Math.random() * Math.PI * 2;
    }

    const g = new THREE.BufferGeometry();
    g.setAttribute("position", new THREE.BufferAttribute(positions, 3));
    g.setAttribute("aScale", new THREE.BufferAttribute(scales, 1));
    g.setAttribute("aPhase", new THREE.BufferAttribute(phases, 1));
    return g;
  }, [count]);

  const uniforms = useMemo(
    () => ({
      uTime: { value: 0 },
      uSize: { value: 26 },
      uPixelRatio: { value: 1 },
      uColor: { value: COLORS.cotton },
    }),
    [],
  );

  useFrame(({ clock, gl }) => {
    const mat = material.current;
    if (!mat) return;
    if (animate) mat.uniforms.uTime.value = clock.elapsedTime;
    mat.uniforms.uPixelRatio.value = gl.getPixelRatio();
  });

  // Keep the field proportionate on very wide viewports.
  const scale = Math.min(1.35, Math.max(0.85, viewport.width / 8));

  return (
    <points geometry={geometry} scale={scale}>
      <shaderMaterial
        ref={material}
        vertexShader={moteVertex}
        fragmentShader={moteFragment}
        uniforms={uniforms}
        transparent
        blending={THREE.AdditiveBlending}
        depthWrite={false}
      />
    </points>
  );
}

/* -------------------------------------------------------------------------- */
/* Scene                                                                       */
/* -------------------------------------------------------------------------- */

function Scene({ animate }: { animate: boolean }) {
  const group = useRef<THREE.Group>(null);
  const parallax = useRef(new THREE.Vector2());

  const points = useMemo(() => {
    const map = new Map<string, THREE.Vector3>();
    for (const node of nodes) {
      map.set(node.id, latLngToVector3(node.lat, node.lng, GLOBE_RADIUS * 1.004));
    }
    return map;
  }, []);

  useFrame(({ pointer }, delta) => {
    if (!group.current || !animate) return;

    // Gentle parallax. The globe leans toward the cursor rather than following
    // it, which keeps the motion calm at the top of the page.
    parallax.current.lerp(pointer, Math.min(1, delta * 2.2));
    group.current.rotation.x = parallax.current.y * 0.12;
    group.current.position.x = parallax.current.x * 0.08;
  });

  return (
    <group ref={group}>
      {/* Framed so the export lanes are the subject: Europe near the centre,
          Pakistan toward the right limb and the Atlantic crossing to the left,
          which is the one orientation that shows most of the routes at once.
          Euler order is XYZ, so the yaw lands the longitude and the pitch then
          brings 30 degrees north up to the equator of the view. */}
      <group rotation={[0.52, -2.2, 0]}>
        <Shell />
        <Graticule />
        <SurfaceDots />

        {routes.map((route, i) => {
          const from = points.get(route.from);
          const to = points.get(route.to);
          if (!from || !to) return null;
          return (
            <ThreadArc
              key={`${route.from}-${route.to}`}
              from={from}
              to={to}
              kind={route.kind}
              index={i}
              animate={animate}
            />
          );
        })}

        {nodes.map((node, i) => {
          const position = points.get(node.id);
          if (!position) return null;
          return (
            <Marker
              key={node.id}
              position={position}
              label={node.label}
              kind={node.kind}
              index={i}
              animate={animate}
            />
          );
        })}
      </group>

      <Motes animate={animate} />
    </group>
  );
}

export default function ThreadGlobe() {
  const reduced = useReducedMotion();
  const animate = !reduced;

  return (
    <Canvas
      camera={{ position: [0, 0.3, 3.55], fov: 42 }}
      dpr={[1, 2]}
      gl={{
        antialias: true,
        alpha: true,
        powerPreference: "high-performance",
      }}
      // One composed frame is enough when motion is not wanted.
      frameloop={animate ? "always" : "demand"}
      style={{ pointerEvents: "auto" }}
    >
      <Scene animate={animate} />

      <OrbitControls
        enableZoom={false}
        enablePan={false}
        enableDamping
        dampingFactor={0.06}
        rotateSpeed={0.4}
        autoRotate={animate}
        autoRotateSpeed={0.36}
        minPolarAngle={Math.PI * 0.24}
        maxPolarAngle={Math.PI * 0.76}
      />
    </Canvas>
  );
}
