// Globe: canvas-rendered 3D wireframe earth using real coastline data
// (Natural Earth 110m via world-atlas TopoJSON). Falls back to a rough
// bounding-box sketch while data loads or if the fetch is blocked.

const { useEffect, useRef, useState } = React;

/* ==========================================================================
   REAL WORLD DATA — fetched once, decoded, rasterized into a land mask.
   ========================================================================== */

const WORLD_DATA_URL = 'https://cdn.jsdelivr.net/npm/world-atlas@2/land-110m.json';

// Module-level cache — fetch once per page.
let _worldCache = null;
let _worldPromise = null;

function fetchWorld() {
  if (_worldCache) return Promise.resolve(_worldCache);
  if (_worldPromise) return _worldPromise;
  _worldPromise = fetch(WORLD_DATA_URL)
    .then(r => r.ok ? r.json() : null)
    .then(topo => {
      if (!topo) return null;
      try {
        const coastlines = topoToRings(topo);     // Array<Array<[lon,lat]>>
        const dots = rasterLandDots(coastlines, 2); // 2° grid
        _worldCache = { coastlines, dots };
        return _worldCache;
      } catch (e) { return null; }
    })
    .catch(() => null);
  return _worldPromise;
}

// Decode TopoJSON arcs → flat [lon, lat] rings per polygon.
// Handles Polygon, MultiPolygon, and GeometryCollection wrappers.
function topoToRings(topo) {
  const { transform, arcs, objects } = topo;
  const [sx, sy] = transform.scale;
  const [tx, ty] = transform.translate;

  // Delta-decode each arc into absolute [lon, lat] points.
  const decoded = arcs.map(arc => {
    let x = 0, y = 0;
    return arc.map(([dx, dy]) => {
      x += dx; y += dy;
      return [x * sx + tx, y * sy + ty];
    });
  });

  const buildRing = (arcIdxList) => {
    const ring = [];
    for (let idx of arcIdxList) {
      const reverse = idx < 0;
      if (reverse) idx = ~idx;
      const seq = decoded[idx];
      const points = reverse ? seq.slice().reverse() : seq;
      // skip the shared endpoint between adjacent arcs
      for (let i = ring.length ? 1 : 0; i < points.length; i++) ring.push(points[i]);
    }
    return ring;
  };

  const rings = [];
  const visit = (geom) => {
    if (!geom) return;
    switch (geom.type) {
      case 'GeometryCollection':
        (geom.geometries || []).forEach(visit);
        break;
      case 'MultiPolygon':
        for (const polygon of geom.arcs) for (const r of polygon) rings.push(buildRing(r));
        break;
      case 'Polygon':
        for (const r of geom.arcs) rings.push(buildRing(r));
        break;
    }
  };
  visit(objects.land);
  return rings;
}

// Rasterize polygons at 720×360 and sample at `step`° to get land dots.
function rasterLandDots(rings, step = 2) {
  const W = 720, H = 360;
  const c = document.createElement('canvas');
  c.width = W; c.height = H;
  const g = c.getContext('2d');
  g.fillStyle = '#fff';
  g.beginPath();
  for (const ring of rings) {
    for (let i = 0; i < ring.length; i++) {
      const [lon, lat] = ring[i];
      const px = (lon + 180) / 360 * W;
      const py = (90 - lat) / 180 * H;
      if (i === 0) g.moveTo(px, py); else g.lineTo(px, py);
    }
    g.closePath();
  }
  g.fill('evenodd'); // handles holes (e.g. inland seas)

  const data = g.getImageData(0, 0, W, H).data;
  const dots = [];
  for (let lat = -85; lat <= 85; lat += step) {
    for (let lon = -180; lon <= 180; lon += step) {
      const px = Math.min(W - 1, Math.floor((lon + 180) / 360 * W));
      const py = Math.min(H - 1, Math.floor((90 - lat) / 180 * H));
      const a = data[(py * W + px) * 4 + 3];
      if (a > 128) dots.push([lat, lon]);
    }
  }
  return dots;
}

/* ==========================================================================
   FALLBACK — rough bounding-box land so the globe still paints before/without
   the TopoJSON load.
   ========================================================================== */
const FALLBACK_LAND_RECTS = [
  [[25,70],[-170,-52]], [[10,25],[-115,-80]], [[60,83],[-55,-15]],
  [[-56,12],[-82,-35]], [[36,71],[-10,40]], [[-35,37],[-17,52]],
  [[12,40],[30,62]], [[45,75],[40,180]], [[5,32],[68,95]],
  [[-10,28],[92,122]], [[20,50],[95,145]], [[30,45],[128,146]],
  [[-44,-10],[112,154]], [[-10,6],[95,140]], [[-85,-65],[-180,180]],
];
const FALLBACK_LAND_DOTS = (() => {
  const dots = [];
  for (let lat = -85; lat <= 85; lat += 3) {
    for (let lon = -180; lon <= 180; lon += 3) {
      for (const [[la1,la2],[lo1,lo2]] of FALLBACK_LAND_RECTS) {
        if (lat>=la1 && lat<=la2 && lon>=lo1 && lon<=lo2) { dots.push([lat,lon]); break; }
      }
    }
  }
  return dots;
})();

/* ==========================================================================
   Scene data
   ========================================================================== */

const MARKERS = [
  { name: 'Reykjavík',    lat: 64.1,  lon: -21.9, slot: 'a' },
  { name: 'London',       lat: 51.5,  lon: -0.1,  slot: 'b' },
  { name: 'Oslo',         lat: 59.9,  lon: 10.7,  slot: 'b' },
  { name: 'Dhaka',        lat: 23.8,  lon: 90.4,  slot: 'a' },
  { name: 'Mumbai',       lat: 19.1,  lon: 72.9,  slot: 'c' },
  { name: 'Singapore',    lat: 1.3,   lon: 103.8, slot: 'a' },
  { name: 'Jakarta',      lat: -6.2,  lon: 106.8, slot: 'a' },
  { name: 'Tokyo',        lat: 35.7,  lon: 139.7, slot: 'c' },
  { name: 'Perth',        lat: -31.9, lon: 115.9, slot: 'a' },
  { name: 'Nairobi',      lat: -1.3,  lon: 36.8,  slot: 'b' },
  { name: 'Johannesburg', lat: -26.2, lon: 28.0,  slot: 'c' },
  { name: 'São Paulo',    lat: -23.5, lon: -46.6, slot: 'c' },
  { name: 'Buenos Aires', lat: -34.6, lon: -58.4, slot: 'a' },
  { name: 'Denver',       lat: 39.7,  lon: -104.9,slot: 'c' },
];

// pairs indexing MARKERS
const ARCS = [
  [0, 2], [2, 1], [1, 9], [9, 4], [4, 3], [3, 5], [5, 6],
  [6, 7], [7, 8], [9, 10], [11, 12], [13, 11], [0, 1],
];

/* ==========================================================================
   3D helpers
   ========================================================================== */

function latLonToXYZ(lat, lon, r = 1) {
  const phi = (90 - lat) * Math.PI / 180;
  const theta = lon * Math.PI / 180;
  // East → +X (right), North → +Y (flipped to top in project()), lon 0° toward viewer.
  return [
    r * Math.sin(phi) * Math.sin(theta),
    r * Math.cos(phi),
    r * Math.sin(phi) * Math.cos(theta),
  ];
}
function rotateY([x,y,z], a) { const c=Math.cos(a), s=Math.sin(a); return [c*x+s*z, y, -s*x+c*z]; }
function rotateX([x,y,z], a) { const c=Math.cos(a), s=Math.sin(a); return [x, c*y-s*z, s*y+c*z]; }

function hexToRgba(hex, a) {
  const n = parseInt(hex.slice(1), 16);
  const r=(n>>16)&255, g=(n>>8)&255, b=n&255;
  return `rgba(${r},${g},${b},${a})`;
}
function hexToRgbTuple(hex) {
  if (!hex || !hex.startsWith('#')) return '88,214,255';
  let h = hex.slice(1);
  if (h.length === 3) h = h.split('').map(c => c+c).join('');
  const n = parseInt(h, 16);
  if (Number.isNaN(n)) return '88,214,255';
  return `${(n>>16)&255},${(n>>8)&255},${n&255}`;
}

function arcPoint(a, b, t) {
  const x = a[0] + (b[0] - a[0]) * t;
  const y = a[1] + (b[1] - a[1]) * t;
  const z = a[2] + (b[2] - a[2]) * t;
  const len = Math.sqrt(x*x + y*y + z*z);
  const alt = 1 + Math.sin(t * Math.PI) * 0.22;
  return [x/len*alt, y/len*alt, z/len*alt];
}

function drawOrbit(ctx, cx, cy, r, yRot, xRot, color) {
  ctx.strokeStyle = color;
  ctx.lineWidth = 1;
  ctx.beginPath();
  const steps = 96;
  for (let i = 0; i <= steps; i++) {
    const t = i / steps * Math.PI * 2;
    let p = [Math.cos(t), 0, Math.sin(t)];
    p = rotateY(p, yRot);
    p = rotateX(p, xRot);
    const px = cx + p[0] * r;
    const py = cy + p[1] * r;
    if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
  }
  ctx.stroke();
}

/* ==========================================================================
   Component
   ========================================================================== */

function Globe() {
  const canvasRef = useRef(null);
  const rafRef = useRef(null);
  const worldRef = useRef({ coastlines: null, dots: FALLBACK_LAND_DOTS });
  // re-render once real data lands — picks up denser dots + coastlines
  const [, bump] = useState(0);

  useEffect(() => {
    let alive = true;
    fetchWorld().then(data => {
      if (!alive || !data) return;
      worldRef.current = data;
      bump(n => n + 1);
    });
    return () => { alive = false; };
  }, []);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    const start = performance.now();
    const size = { w: 600, h: 600, dpr: 1 };

    const resize = () => {
      const rect = canvas.getBoundingClientRect();
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      canvas.width = rect.width * dpr;
      canvas.height = rect.height * dpr;
      size.w = rect.width; size.h = rect.height; size.dpr = dpr;
    };
    resize();
    const ro = new ResizeObserver(resize);
    ro.observe(canvas);

    const PARTICLES = Array.from({ length: 32 }, () => ({
      lat: Math.random() * 180 - 90,
      lon: Math.random() * 360 - 180,
      speed: 0.1 + Math.random() * 0.3,
      phase: Math.random() * Math.PI * 2,
    }));

    const resolveTheme = () => {
      const cs = getComputedStyle(document.documentElement);
      const paletteEl = document.querySelector('[data-palette]') || document.documentElement;
      const palette = paletteEl.getAttribute('data-palette') || 'midnight';
      const accent = (cs.getPropertyValue('--accent') || '#58d6ff').trim();
      const accent2 = (cs.getPropertyValue('--accent-2') || '#7bc99d').trim();
      const accent3 = (cs.getPropertyValue('--accent-3') || '#e8a948').trim();

      const schemes = {
        midnight: {
          baseInner: '#1a2c52', baseMid: '#0d1a30', baseOuter: '#060d1c',
          atmo: '#58d6ff', land: '145, 215, 235',
          orbit1: '88,214,255', orbit2: '125,205,155', termAlpha: 0.45,
        },
        bone: {
          baseInner: '#2e4a36', baseMid: '#1b2e22', baseOuter: '#0f1d14',
          atmo: accent || '#2d5a3d', land: '232, 212, 158',
          orbit1: '168,110,72', orbit2: '106,139,82', termAlpha: 0.55,
        },
        paper: {
          baseInner: '#1f3a66', baseMid: '#10203f', baseOuter: '#070f22',
          atmo: accent || '#1e5aa8', land: '200, 220, 245',
          orbit1: '30,90,168', orbit2: '46,117,96', termAlpha: 0.55,
        },
        linen: {
          baseInner: '#3a2820', baseMid: '#241611', baseOuter: '#130a08',
          atmo: accent3 || '#9e4a2a', land: '215, 190, 150',
          orbit1: '158,74,42', orbit2: '107,132,83', termAlpha: 0.55,
        },
      };
      const s = schemes[palette] || schemes.midnight;
      return { ...s, markers: { a: accent, b: accent2, c: accent3 } };
    };

    const tick = (t) => {
      const elapsed = (t - start) / 1000;
      const { w, h, dpr } = size;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      ctx.clearRect(0, 0, w, h);

      const theme = resolveTheme();
      const atmoRgb = hexToRgbTuple(theme.atmo);
      const world = worldRef.current;

      const cx = w / 2, cy = h / 2;
      const R = Math.min(w, h) * 0.42;
      const yRot = elapsed * 0.08;
      const xRot = 0.35; // positive tilt → North Pole leans toward viewer

      const project = (lat, lon, r = 1) => {
        let p = latLonToXYZ(lat, lon, r);
        p = rotateY(p, yRot);
        p = rotateX(p, xRot);
        // Canvas Y goes down; flip so North appears at the top of the screen.
        return [p[0], -p[1], p[2]];
      };

      // ——— Atmosphere glow ———
      // Outer radius must stay below canvas half-width (≈1.19R) or the circle
      // gets cropped by the canvas rectangle and shows as a visible box edge.
      const gradAtm = ctx.createRadialGradient(cx, cy, R * 0.97, cx, cy, R * 1.15);
      gradAtm.addColorStop(0, `rgba(${atmoRgb},0.34)`);
      gradAtm.addColorStop(0.55, `rgba(${atmoRgb},0.1)`);
      gradAtm.addColorStop(1, `rgba(${atmoRgb},0)`);
      ctx.fillStyle = gradAtm;
      ctx.beginPath();
      ctx.arc(cx, cy, R * 1.15, 0, Math.PI * 2);
      ctx.fill();

      // ——— Globe body (radial for depth) ———
      const gradBase = ctx.createRadialGradient(
        cx - R * 0.35, cy - R * 0.35, R * 0.2,
        cx, cy, R * 1.05
      );
      gradBase.addColorStop(0, theme.baseInner);
      gradBase.addColorStop(0.55, theme.baseMid);
      gradBase.addColorStop(1, theme.baseOuter);
      ctx.fillStyle = gradBase;
      ctx.beginPath();
      ctx.arc(cx, cy, R, 0, Math.PI * 2);
      ctx.fill();

      // ——— Outer ring ———
      ctx.strokeStyle = `rgba(${atmoRgb},0.35)`;
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.arc(cx, cy, R, 0, Math.PI * 2);
      ctx.stroke();

      // ——— Graticule (lat/lon lines) ———
      ctx.strokeStyle = 'rgba(200,215,235,0.11)';
      ctx.lineWidth = 0.5;
      for (let lat = -75; lat <= 75; lat += 15) {
        // emphasize equator
        ctx.lineWidth = lat === 0 ? 0.9 : 0.5;
        ctx.strokeStyle = lat === 0
          ? `rgba(${atmoRgb},0.28)`
          : 'rgba(200,215,235,0.11)';
        ctx.beginPath();
        let started = false;
        for (let lon = -180; lon <= 180; lon += 4) {
          const [x,y,z] = project(lat, lon, 1);
          if (z < 0) { started = false; continue; }
          const px = cx + x*R, py = cy + y*R;
          if (!started) { ctx.moveTo(px, py); started = true; } else ctx.lineTo(px, py);
        }
        ctx.stroke();
      }
      for (let lon = -180; lon < 180; lon += 15) {
        ctx.lineWidth = lon === 0 ? 0.9 : 0.5;
        ctx.strokeStyle = lon === 0
          ? `rgba(${atmoRgb},0.28)`
          : 'rgba(200,215,235,0.11)';
        ctx.beginPath();
        let started = false;
        for (let lat = -90; lat <= 90; lat += 4) {
          const [x,y,z] = project(lat, lon, 1);
          if (z < 0) { started = false; continue; }
          const px = cx + x*R, py = cy + y*R;
          if (!started) { ctx.moveTo(px, py); started = true; } else ctx.lineTo(px, py);
        }
        ctx.stroke();
      }

      // ——— Coastline outlines (from real data) ———
      if (world.coastlines) {
        const emitPath = (pts, style, width) => {
          if (pts.length < 2) return;
          ctx.strokeStyle = style;
          ctx.lineWidth = width;
          ctx.beginPath();
          for (let i = 0; i < pts.length; i++) {
            const [px, py] = pts[i];
            if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
          }
          ctx.stroke();
        };
        for (const ring of world.coastlines) {
          let path = [];
          for (let i = 0; i < ring.length; i++) {
            const [lon, lat] = ring[i];
            const [x, y, z] = project(lat, lon, 1.002);
            if (z >= -0.01) {
              path.push([cx + x*R, cy + y*R]);
            } else if (path.length) {
              // glow pass + crisp pass for the visible run
              emitPath(path, `rgba(${atmoRgb},0.14)`, 2.4);
              emitPath(path, `rgba(${atmoRgb},0.55)`, 0.7);
              path = [];
            }
          }
          if (path.length) {
            emitPath(path, `rgba(${atmoRgb},0.14)`, 2.4);
            emitPath(path, `rgba(${atmoRgb},0.55)`, 0.7);
          }
        }
      }

      // ——— Land dots (real-data density when loaded) ———
      for (const [lat, lon] of world.dots) {
        const [x, y, z] = project(lat, lon, 1.001);
        if (z < -0.05) continue;
        const alpha = Math.max(0.15, z * 0.9 + 0.2);
        const sz = 0.85 + z * 0.85;
        ctx.fillStyle = `rgba(${theme.land}, ${alpha})`;
        ctx.beginPath();
        ctx.arc(cx + x*R, cy + y*R, sz, 0, Math.PI * 2);
        ctx.fill();
      }

      // ——— Markers & pulses ———
      MARKERS.forEach((m, idx) => {
        const [x, y, z] = project(m.lat, m.lon, 1.01);
        if (z < 0) return;
        const px = cx + x*R, py = cy + y*R;
        const color = theme.markers[m.slot] || theme.markers.a;

        const pulse = (elapsed * 0.8 + idx * 0.3) % 2;
        if (pulse < 1.2) {
          ctx.strokeStyle = hexToRgba(color, (1 - pulse / 1.2) * 0.6);
          ctx.lineWidth = 1;
          ctx.beginPath();
          ctx.arc(px, py, 4 + pulse * 14, 0, Math.PI * 2);
          ctx.stroke();
        }
        ctx.fillStyle = color;
        ctx.beginPath();
        ctx.arc(px, py, 2.4, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = hexToRgba(color, 0.18);
        ctx.beginPath();
        ctx.arc(px, py, 5, 0, Math.PI * 2);
        ctx.fill();
      });

      // ——— Arcs (data links between markers) ———
      ARCS.forEach(([a, b], i) => {
        const ma = MARKERS[a], mb = MARKERS[b];
        const pa = project(ma.lat, ma.lon, 1.01);
        const pb = project(mb.lat, mb.lon, 1.01);
        if (pa[2] < 0 && pb[2] < 0) return;

        const period = 4;
        const p = ((elapsed + i * 0.6) % period) / period;
        const steps = 36;
        ctx.lineWidth = 1.2;

        for (let s = 0; s < steps; s++) {
          const t0 = s / steps;
          const t1 = (s + 1) / steps;
          const dist = Math.min(Math.abs(t0 - p), Math.abs(t0 - p + 1), Math.abs(t0 - p - 1));
          if (dist > 0.35) continue;
          const alpha = Math.max(0, 1 - dist / 0.35);
          const q0 = arcPoint(pa, pb, t0);
          const q1 = arcPoint(pa, pb, t1);
          if (q0[2] < -0.1 || q1[2] < -0.1) continue;
          ctx.strokeStyle = `rgba(${atmoRgb},${alpha * 0.8})`;
          ctx.beginPath();
          ctx.moveTo(cx + q0[0]*R, cy + q0[1]*R);
          ctx.lineTo(cx + q1[0]*R, cy + q1[1]*R);
          ctx.stroke();
        }
      });

      // ——— Swirling data particles ———
      PARTICLES.forEach((pt) => {
        const lon = pt.lon + elapsed * pt.speed * 40;
        const [x, y, z] = project(pt.lat, lon, 1.12 + Math.sin(elapsed + pt.phase) * 0.03);
        if (z < 0) return;
        const alpha = 0.3 + z * 0.5;
        ctx.fillStyle = `rgba(${atmoRgb},${alpha})`;
        ctx.beginPath();
        ctx.arc(cx + x*R, cy + y*R, 1.2, 0, Math.PI * 2);
        ctx.fill();
      });

      // ——— Orbital rings ——— (kept inside the canvas-inscribed circle)
      drawOrbit(ctx, cx, cy, R * 1.09, yRot * 2,   xRot + 0.1, `rgba(${theme.orbit1},0.32)`);
      drawOrbit(ctx, cx, cy, R * 1.16, -yRot * 1.5, xRot - 0.3, `rgba(${theme.orbit2},0.22)`);

      // ——— Night-side terminator ———
      // Sun anchor sits just outside the upper-left of the globe, and the
      // gradient radius is comparable to R — so its isocurves are the same
      // scale as the sphere and read as an actual day/night curve.
      const sunX = cx - R * 0.3;
      const sunY = cy - R * 0.3;
      const termGrad = ctx.createRadialGradient(
        sunX, sunY, 0,
        sunX, sunY, R * 1.5
      );
      // Stops are placed at fractions of the outer radius (1.5R):
      //   0.53 = 0.80R from sun  (edge of sunlit region)
      //   0.67 = 1.00R from sun  (terminator midline — curves across sphere)
      //   0.87 = 1.30R from sun  (deep shadow)
      termGrad.addColorStop(0.0,  'rgba(0,0,0,0)');
      termGrad.addColorStop(0.53, 'rgba(0,0,0,0)');
      termGrad.addColorStop(0.67, `rgba(0,0,0,${theme.termAlpha * 0.30})`);
      termGrad.addColorStop(0.87, `rgba(0,0,0,${theme.termAlpha * 0.85})`);
      termGrad.addColorStop(1.0,  `rgba(0,0,0,${theme.termAlpha})`);
      ctx.save();
      ctx.beginPath();
      ctx.arc(cx, cy, R, 0, Math.PI * 2);
      ctx.clip();
      ctx.fillStyle = termGrad;
      ctx.fillRect(cx - R, cy - R, R * 2, R * 2);
      ctx.restore();

      // ——— Rim highlight (subtle inner glow, sun side) ———
      const rimGrad = ctx.createRadialGradient(
        cx - R * 0.45, cy - R * 0.45, R * 0.8,
        cx, cy, R * 1.02
      );
      rimGrad.addColorStop(0, `rgba(${atmoRgb},0)`);
      rimGrad.addColorStop(0.9, `rgba(${atmoRgb},0)`);
      rimGrad.addColorStop(1, `rgba(${atmoRgb},0.2)`);
      ctx.save();
      ctx.beginPath();
      ctx.arc(cx, cy, R, 0, Math.PI * 2);
      ctx.clip();
      ctx.fillStyle = rimGrad;
      ctx.fillRect(cx - R, cy - R, R * 2, R * 2);
      ctx.restore();

      // ——— Crosshair ticks (just outside the globe rim) ———
      ctx.strokeStyle = `rgba(${atmoRgb},0.5)`;
      ctx.lineWidth = 1;
      const tickLen = 6;
      const tickOff = R * 1.05;
      [0, Math.PI/2, Math.PI, Math.PI*3/2].forEach(a => {
        const x1 = cx + Math.cos(a) * tickOff;
        const y1 = cy + Math.sin(a) * tickOff;
        const x2 = cx + Math.cos(a) * (tickOff + tickLen);
        const y2 = cy + Math.sin(a) * (tickOff + tickLen);
        ctx.beginPath();
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.stroke();
      });

      rafRef.current = requestAnimationFrame(tick);
    };
    rafRef.current = requestAnimationFrame(tick);

    return () => { cancelAnimationFrame(rafRef.current); ro.disconnect(); };
  }, []);

  return <canvas ref={canvasRef} className="globe-canvas" />;
}

window.Globe = Globe;
