// Imported from `nodes` rather than `geo` on purpose. `geo` pulls in three.js,
// and this component renders on first paint.
import { nodes, routes } from "./nodes";

/**
 * The globe without WebGL.
 *
 * Same coordinates, same routes, projected orthographically with plain maths so
 * it pulls none of three.js into the bundle. Shown when a WebGL context cannot
 * be created, and used as the poster frame while the real scene loads.
 */

const SIZE = 520;
const CX = SIZE / 2;
const CY = SIZE / 2;
const R = 190;

// Matches the scene's opening orientation, so the two views agree.
const YAW = -1.35;
const PITCH = 0.22;

type P3 = { x: number; y: number; z: number };

function toVector(lat: number, lng: number, radius = 1): P3 {
  const phi = (90 - lat) * (Math.PI / 180);
  const theta = (lng + 180) * (Math.PI / 180);
  return {
    x: -radius * Math.sin(phi) * Math.cos(theta),
    y: radius * Math.cos(phi),
    z: radius * Math.sin(phi) * Math.sin(theta),
  };
}

function rotate(p: P3): P3 {
  // Yaw about Y, then pitch about X.
  const cy = Math.cos(YAW);
  const sy = Math.sin(YAW);
  const x1 = p.x * cy + p.z * sy;
  const z1 = -p.x * sy + p.z * cy;

  const cx = Math.cos(PITCH);
  const sx = Math.sin(PITCH);
  const y2 = p.y * cx - z1 * sx;
  const z2 = p.y * sx + z1 * cx;

  return { x: x1, y: y2, z: z2 };
}

function project(p: P3) {
  const r = rotate(p);
  // Under orthographic projection a point is hidden exactly when it is behind
  // the globe and inside its silhouette. Lifted arc points can sit behind the
  // centre and still be visible, which is why the radius matters here.
  const silhouette = Math.hypot(r.x, r.y);
  return {
    x: CX + r.x * R,
    y: CY - r.y * R,
    visible: r.z > -0.02 || silhouette > 1,
  };
}

function length3(p: P3): number {
  return Math.hypot(p.x, p.y, p.z);
}

function normalize(p: P3): P3 {
  const l = length3(p) || 1;
  return { x: p.x / l, y: p.y / l, z: p.z / l };
}

/** Great circle interpolation between two points on the sphere. */
function slerp(a: P3, b: P3, t: number): P3 {
  const dot = Math.min(1, Math.max(-1, a.x * b.x + a.y * b.y + a.z * b.z));
  const omega = Math.acos(dot);

  if (omega < 1e-6) return a;

  const s = Math.sin(omega);
  const wa = Math.sin((1 - t) * omega) / s;
  const wb = Math.sin(t * omega) / s;

  return {
    x: a.x * wa + b.x * wb,
    y: a.y * wa + b.y * wb,
    z: a.z * wa + b.z * wb,
  };
}

/**
 * A route sampled along its true great circle and lifted off the surface, then
 * split into the spans that face the viewer. Anything on the far side simply
 * stops, the way a thread does when it goes round the back of a spool.
 */
function routePaths(from: P3, to: P3, steps = 96): string[] {
  const dot = Math.min(1, Math.max(-1, from.x * to.x + from.y * to.y + from.z * to.z));
  const lift = 0.06 + Math.acos(dot) * 0.13;

  const paths: string[] = [];
  let current: string[] = [];

  for (let i = 0; i <= steps; i++) {
    const t = i / steps;
    const base = normalize(slerp(from, to, t));
    const radius = 1 + lift * Math.sin(Math.PI * t);
    const point = project({
      x: base.x * radius,
      y: base.y * radius,
      z: base.z * radius,
    });

    if (point.visible) {
      current.push(`${point.x.toFixed(1)} ${point.y.toFixed(1)}`);
    } else {
      if (current.length > 1) paths.push(`M${current.join(" L")}`);
      current = [];
    }
  }

  if (current.length > 1) paths.push(`M${current.join(" L")}`);
  return paths;
}

function projectLatLng(lat: number, lng: number, radius = 1) {
  return project(toVector(lat, lng, radius));
}

/** Front facing spans of a lat or lng ring, so nothing draws through the globe. */
function ringPaths(
  sample: (t: number) => { lat: number; lng: number },
  steps: number,
): string[] {
  const paths: string[] = [];
  let current: string[] = [];

  for (let i = 0; i <= steps; i++) {
    const { lat, lng } = sample(i / steps);
    const point = projectLatLng(lat, lng);

    if (point.visible) {
      current.push(`${point.x.toFixed(1)} ${point.y.toFixed(1)}`);
    } else if (current.length > 1) {
      paths.push(`M${current.join(" L")}`);
      current = [];
    } else {
      current = [];
    }
  }

  if (current.length > 1) paths.push(`M${current.join(" L")}`);
  return paths;
}

export function GlobeFallback({ className }: { className?: string }) {
  const parallels = Array.from({ length: 12 }, (_, i) => {
    const lat = -75 + i * 15;
    return ringPaths((t) => ({ lat, lng: -180 + t * 360 }), 96);
  }).flat();

  const meridians = Array.from({ length: 24 }, (_, i) => {
    const lng = -180 + i * 15;
    return ringPaths((t) => ({ lat: -90 + t * 180, lng }), 64);
  }).flat();

  const nodeMap = new Map(nodes.map((n) => [n.id, n]));

  return (
    <svg
      viewBox={`0 0 ${SIZE} ${SIZE}`}
      className={className}
      role="img"
      aria-label="Globe showing shipping routes from Multan and Lahore, Pakistan, to the United Kingdom, Europe, the United States and South America"
    >
      <defs>
        <radialGradient id="gf-shell" cx="34%" cy="28%" r="76%">
          <stop offset="0%" stopColor="#243668" />
          <stop offset="70%" stopColor="#131c3c" />
          <stop offset="100%" stopColor="#0a0f22" />
        </radialGradient>
        <radialGradient id="gf-rim" cx="50%" cy="50%" r="50%">
          <stop offset="82%" stopColor="#6f8ae0" stopOpacity="0" />
          <stop offset="97%" stopColor="#6f8ae0" stopOpacity="0.5" />
          <stop offset="100%" stopColor="#6f8ae0" stopOpacity="0" />
        </radialGradient>
      </defs>

      <circle cx={CX} cy={CY} r={R} fill="url(#gf-shell)" />

      <g stroke="#3a56a6" strokeOpacity="0.34" strokeWidth="0.7" fill="none">
        {[...parallels, ...meridians].map((d, i) => (
          <path key={i} d={d} />
        ))}
      </g>

      <circle cx={CX} cy={CY} r={R * 1.06} fill="url(#gf-rim)" />

      {/* Trade routes, lifted off the surface the same way the 3D scene lifts
          them, and cut where they pass behind the globe. */}
      <g fill="none" strokeLinecap="round">
        {routes.map((route) => {
          const from = nodeMap.get(route.from);
          const to = nodeMap.get(route.to);
          if (!from || !to) return null;

          const spans = routePaths(
            toVector(from.lat, from.lng),
            toVector(to.lat, to.lng),
          );

          return spans.map((d, i) => (
            <path
              key={`${route.from}-${route.to}-${i}`}
              d={d}
              stroke={route.kind === "prospect" ? "#8a8f9c" : "#e0a458"}
              strokeOpacity={route.kind === "prospect" ? 0.45 : 0.85}
              strokeWidth={route.kind === "prospect" ? 1 : 1.5}
            />
          ));
        })}
      </g>

      <g>
        {nodes.map((node) => {
          const p = projectLatLng(node.lat, node.lng);
          if (!p.visible) return null;

          const color =
            node.kind === "origin"
              ? "#e0a458"
              : node.kind === "prospect"
                ? "#8a8f9c"
                : "#f5f1e8";

          return (
            <g key={node.id}>
              <circle
                cx={p.x}
                cy={p.y}
                r={node.kind === "origin" ? 8 : 6}
                fill={color}
                opacity="0.16"
              />
              <circle
                cx={p.x}
                cy={p.y}
                r={node.kind === "origin" ? 3.2 : 2.4}
                fill={color}
              />
            </g>
          );
        })}
      </g>
    </svg>
  );
}
