import * as THREE from "three";
import { GLOBE_RADIUS } from "./nodes";

/**
 * Geometry helpers for the WebGL globe.
 *
 * Everything in here pulls in three.js, so it must only ever be imported from
 * inside the lazily loaded scene. The coordinates themselves live in
 * `nodes.ts`, which imports nothing and is safe for the SVG fallback to read.
 */

export { GLOBE_RADIUS, nodes, routes } from "./nodes";
export type { GeoNode } from "./nodes";

/** Latitude and longitude in degrees to a point on the sphere. */
export function latLngToVector3(
  lat: number,
  lng: number,
  radius = GLOBE_RADIUS,
): THREE.Vector3 {
  const phi = (90 - lat) * (Math.PI / 180);
  const theta = (lng + 180) * (Math.PI / 180);

  return new THREE.Vector3(
    -radius * Math.sin(phi) * Math.cos(theta),
    radius * Math.cos(phi),
    radius * Math.sin(phi) * Math.sin(theta),
  );
}

/**
 * A shipping route as a curve that lifts off the surface. The further apart the
 * two ports, the higher the thread arcs, which is what makes a set of routes
 * read as a set rather than a tangle.
 */
export function arcCurve(
  from: THREE.Vector3,
  to: THREE.Vector3,
  spread = 0,
): THREE.QuadraticBezierCurve3 {
  const distance = from.distanceTo(to);

  // `spread` nests routes that share nearly the same great circle at slightly
  // different heights. Multan and Lahore are only three degrees apart, so
  // without it their lanes to Europe overlay into one thick smear.
  const lift = 1 + distance * 0.42 + spread * 0.055;

  const mid = from
    .clone()
    .add(to)
    .multiplyScalar(0.5)
    .normalize()
    .multiplyScalar(GLOBE_RADIUS * lift);

  return new THREE.QuadraticBezierCurve3(from.clone(), mid, to.clone());
}

/** Latitude and longitude ring geometry for the globe cage. */
export function graticuleGeometry(
  latSteps = 13,
  lngSteps = 24,
  segments = 128,
): THREE.BufferGeometry {
  const positions: number[] = [];

  // Parallels.
  for (let i = 1; i < latSteps; i++) {
    const lat = -90 + (180 * i) / latSteps;
    for (let s = 0; s < segments; s++) {
      const a = latLngToVector3(lat, -180 + (360 * s) / segments);
      const b = latLngToVector3(lat, -180 + (360 * (s + 1)) / segments);
      positions.push(a.x, a.y, a.z, b.x, b.y, b.z);
    }
  }

  // Meridians.
  for (let i = 0; i < lngSteps; i++) {
    const lng = -180 + (360 * i) / lngSteps;
    for (let s = 0; s < segments / 2; s++) {
      const a = latLngToVector3(-90 + (180 * s) / (segments / 2), lng);
      const b = latLngToVector3(-90 + (180 * (s + 1)) / (segments / 2), lng);
      positions.push(a.x, a.y, a.z, b.x, b.y, b.z);
    }
  }

  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute(
    "position",
    new THREE.Float32BufferAttribute(positions, 3),
  );
  return geometry;
}

/** Evenly distributed points on a sphere, used for the thread dot field. */
export function fibonacciSphere(count: number, radius = GLOBE_RADIUS): Float32Array {
  const points = new Float32Array(count * 3);
  const golden = Math.PI * (3 - Math.sqrt(5));

  for (let i = 0; i < count; i++) {
    const y = 1 - (i / (count - 1)) * 2;
    const r = Math.sqrt(Math.max(0, 1 - y * y));
    const theta = golden * i;

    points[i * 3] = Math.cos(theta) * r * radius;
    points[i * 3 + 1] = y * radius;
    points[i * 3 + 2] = Math.sin(theta) * r * radius;
  }

  return points;
}
