// Realistic rotating globe powered by d3-geo + world-atlas TopoJSON.
// Country geometry comes from naturalearthdata.com (110m, public domain),
// served by jsdelivr.
// Visited countries are highlighted in the accent color.

const { useState: useGS, useEffect: useGE, useRef: useGR, useMemo: useGM } = React;

const GLOBE_R = 170;
const GLOBE_CX = 200;
const GLOBE_CY = 200;
const TILT_DEG = 18;

function deg2rad(d) {return d * Math.PI / 180;}

// Cities Simone has been to. `kind: "lived"` marks places he's called home;
// everything else is travel. The country counter is derived at render time.
const PLACES = [
// Italy
{ country: "Italy", city: "Rome", lat: 41.9028, lon: 12.4964, kind: "lived" },
{ country: "Italy", city: "Milan", lat: 45.4642, lon: 9.1900, kind: "travel" },
{ country: "Italy", city: "Naples", lat: 40.8518, lon: 14.2681, kind: "travel" },
{ country: "Italy", city: "Florence", lat: 43.7696, lon: 11.2558, kind: "travel" },
{ country: "Italy", city: "Turin", lat: 45.0703, lon: 7.6869, kind: "travel" },
// Germany
{ country: "Germany", city: "Saarbrücken", lat: 49.2402, lon: 6.9969, kind: "lived" },
{ country: "Germany", city: "Cologne", lat: 50.9375, lon: 6.9603, kind: "lived" },
{ country: "Germany", city: "Berlin", lat: 52.5200, lon: 13.4050, kind: "travel" },
{ country: "Germany", city: "Tübingen", lat: 48.5216, lon: 9.0576, kind: "travel" },
{ country: "Germany", city: "Stuttgart", lat: 48.7758, lon: 9.1829, kind: "travel" },
{ country: "Germany", city: "Dortmund", lat: 51.5136, lon: 7.4653, kind: "travel" },
{ country: "Germany", city: "Düsseldorf", lat: 51.2277, lon: 6.7735, kind: "travel" },
{ country: "Germany", city: "Aachen", lat: 50.7753, lon: 6.0839, kind: "travel" },
// UK
{ country: "UK", city: "London", lat: 51.5074, lon: -0.1278, kind: "lived" },
{ country: "UK", city: "Cambridge", lat: 52.2053, lon: 0.1218, kind: "lived" },
// France
{ country: "France", city: "Paris", lat: 48.8566, lon: 2.3522, kind: "travel" },
{ country: "France", city: "Strasbourg", lat: 48.5734, lon: 7.7521, kind: "travel" },
// Spain (incl. Gibraltar)
{ country: "Spain", city: "Barcelona", lat: 41.3851, lon: 2.1734, kind: "travel" },
{ country: "Spain", city: "Valencia", lat: 39.4699, lon: -0.3763, kind: "travel" },
{ country: "Spain", city: "Málaga", lat: 36.7213, lon: -4.4214, kind: "travel" },
{ country: "Spain", city: "Marbella", lat: 36.5099, lon: -4.8856, kind: "travel" },
{ country: "Spain", city: "Gibraltar", lat: 36.1408, lon: -5.3536, kind: "travel" },
// Netherlands
{ country: "Netherlands", city: "Amsterdam", lat: 52.3676, lon: 4.9041, kind: "travel" },
{ country: "Netherlands", city: "Rotterdam", lat: 51.9244, lon: 4.4777, kind: "travel" },
// Belgium
{ country: "Belgium", city: "Brussels", lat: 50.8503, lon: 4.3517, kind: "travel" },
{ country: "Belgium", city: "Ghent", lat: 51.0543, lon: 3.7174, kind: "travel" },
// Austria
{ country: "Austria", city: "Vienna", lat: 48.2082, lon: 16.3738, kind: "travel" },
{ country: "Austria", city: "Hallstatt", lat: 47.5622, lon: 13.6493, kind: "travel" },
// Czech Republic
{ country: "Czech Republic", city: "Prague", lat: 50.0755, lon: 14.4378, kind: "travel" },
// Switzerland
{ country: "Switzerland", city: "Zurich", lat: 47.3769, lon: 8.5417, kind: "travel" },
// Croatia
{ country: "Croatia", city: "Split", lat: 43.5081, lon: 16.4402, kind: "travel" },
// Greece
{ country: "Greece", city: "Rhodes", lat: 36.4341, lon: 28.2176, kind: "travel" },
{ country: "Greece", city: "Kos", lat: 36.8938, lon: 27.2877, kind: "travel" },
// Iceland
{ country: "Iceland", city: "Reykjavík", lat: 64.1466, lon: -21.9426, kind: "travel" },
{ country: "Iceland", city: "Arnarstapi", lat: 64.7693, lon: -23.6225, kind: "travel" },
{ country: "Iceland", city: "Búðir", lat: 64.8224, lon: -23.3848, kind: "travel" },
{ country: "Iceland", city: "Vík í Mýrdal", lat: 63.4194, lon: -19.0083, kind: "travel" },
{ country: "Iceland", city: "Höfn", lat: 64.2538, lon: -15.2089, kind: "travel" },
// USA
{ country: "USA", city: "Honolulu", lat: 21.3099, lon: -157.8581, kind: "travel" },
{ country: "USA", city: "Seattle", lat: 47.6062, lon: -122.3321, kind: "travel" },
// Canada
{ country: "Canada", city: "Vancouver", lat: 49.2827, lon: -123.1207, kind: "travel" },
{ country: "Canada", city: "Banff", lat: 51.1784, lon: -115.5708, kind: "travel" },
// Brazil
{ country: "Brazil", city: "Rio de Janeiro", lat: -22.9068, lon: -43.1729, kind: "travel" },
// Peru
{ country: "Peru", city: "Lima", lat: -12.0464, lon: -77.0428, kind: "travel" },
{ country: "Peru", city: "Ica", lat: -14.0681, lon: -75.7286, kind: "travel" },
{ country: "Peru", city: "Pisco", lat: -13.7000, lon: -76.2167, kind: "travel" },
{ country: "Peru", city: "Cusco", lat: -13.5320, lon: -71.9675, kind: "travel" },
{ country: "Peru", city: "Arequipa", lat: -16.4090, lon: -71.5375, kind: "travel" },
{ country: "Peru", city: "Puno", lat: -15.8402, lon: -70.0219, kind: "travel" }];


// Unique countries visited (used for the counter only).
const COUNTRY_COUNT = new Set(PLACES.map((p) => p.country)).size;

function Globe() {
  const [lambda, setLambda] = useGS(deg2rad(-10));
  const [tilt, setTilt] = useGS(deg2rad(TILT_DEG));
  const [hover, setHover] = useGS(null);
  const [dragging, setDragging] = useGS(false);
  const [features, setFeatures] = useGS(null);
  const targetLambdaRef = useGR(null);
  const dragRef = useGR({ dragging: false, startX: 0, startY: 0, startLambda: 0, startTilt: 0 });
  const rafRef = useGR(0);
  const lastRef = useGR(performance.now());

  // Load world-atlas TopoJSON once.
  useGE(() => {
    let cancelled = false;
    fetch("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json").
    then((r) => r.json()).
    then((topo) => {
      if (cancelled) return;
      const fc = window.topojson.feature(topo, topo.objects.countries);
      setFeatures(fc.features);
    }).
    catch((e) => {
      // eslint-disable-next-line no-console
      console.warn("Failed to load world-atlas:", e);
    });
    return () => {cancelled = true;};
  }, []);

  // When a pin is hovered, freeze rotation in place (we no longer lerp the
  // globe to center the pin — instead we zoom into the pin's current screen
  // position via transform-origin).
  useGE(() => {targetLambdaRef.current = null;}, [hover]);

  // Auto-rotate loop — paused while hovering or dragging.
  useGE(() => {
    function tick(now) {
      const dt = (now - lastRef.current) / 1000;
      lastRef.current = now;
      if (dragRef.current.dragging || hover) {
        rafRef.current = requestAnimationFrame(tick);
        return;
      }
      setLambda((l) => l - dt * deg2rad(4));
      rafRef.current = requestAnimationFrame(tick);
    }
    rafRef.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(rafRef.current);
  }, [hover]);

  function onPointerDown(e) {
    e.preventDefault();
    dragRef.current = {
      dragging: true,
      startX: e.clientX, startY: e.clientY,
      startLambda: lambda, startTilt: tilt
    };
    setDragging(true);
    e.target.setPointerCapture && e.target.setPointerCapture(e.pointerId);
  }
  function onPointerMove(e) {
    if (!dragRef.current.dragging) return;
    const dx = e.clientX - dragRef.current.startX;
    const dy = e.clientY - dragRef.current.startY;
    setLambda(dragRef.current.startLambda + dx * 0.012);
    // Vertical drag tilts. Clamp so the north pole never crosses past
    // horizontal (no upside-down globe).
    const TILT_MIN = deg2rad(-65);
    const TILT_MAX = deg2rad(80);
    const next = dragRef.current.startTilt + dy * 0.012;
    setTilt(Math.max(TILT_MIN, Math.min(TILT_MAX, next)));
  }
  function onPointerUp() {
    dragRef.current.dragging = false;
    setDragging(false);
    lastRef.current = performance.now();
  }

  // d3 orthographic projection — built once, mutated each render.
  const projection = useGM(() =>
  window.d3.geoOrthographic().
  scale(GLOBE_R).
  translate([GLOBE_CX, GLOBE_CY]).
  clipAngle(90),
  []);
  projection.rotate([lambda * 180 / Math.PI, -(tilt * 180 / Math.PI), 0]);
  const pathGen = window.d3.geoPath(projection);
  const graticule = window.d3.geoGraticule().step([30, 30]);

  // Project pins and decide visibility via great-circle distance from view center.
  const rot = projection.rotate();
  const center = [-rot[0], -rot[1]];
  function projectPin(lon, lat) {
    const dist = window.d3.geoDistance([lon, lat], center);
    const xy = projection([lon, lat]) || [0, 0];
    return { x: xy[0], y: xy[1], dist, visible: dist <= Math.PI / 2 };
  }
  const pins = PLACES.map((p) => ({ ...p, proj: projectPin(p.lon, p.lat) }));

  // Anti-collision mini labels — only lived-in cities get a persistent label.
  const candidates = pins.
  filter((p) => p.kind === "lived" && p.proj.visible && p.proj.dist < 1.2 && p.city !== hover).
  sort((a, b) => a.proj.dist - b.proj.dist);
  const labeledList = [];
  for (const c of candidates) {
    const tooClose = labeledList.some(
      (q) => Math.hypot(q.proj.x - c.proj.x, q.proj.y - c.proj.y) < 26
    );
    if (!tooClose) labeledList.push(c);
  }
  const labelable = labeledList.reduce((acc, p) => {acc[p.city] = true;return acc;}, {});

  // Readout
  let readout = "—";
  if (hover) {
    const p = pins.find((x) => x.city === hover);
    if (p) readout = `${p.city.toUpperCase()} · ${p.country}`;
  } else {
    const centerLon = -lambda * 180 / Math.PI;
    let cl = (centerLon + 540) % 360 - 180;
    readout = `view · ${Math.abs(cl).toFixed(0)}°${cl >= 0 ? "E" : "W"}`;
  }

  // Sort pins so front-most render on top.
  pins.sort((a, b) => b.proj.dist - a.proj.dist);

  // The hovered pin's screen position becomes the zoom origin.
  const hoveredPin = hover ? pins.find((x) => x.city === hover) : null;
  const zoomOriginStyle = hoveredPin ?
  { transformOrigin: `${hoveredPin.proj.x / 400 * 100}% ${hoveredPin.proj.y / 400 * 100}%` } :
  { transformOrigin: "50% 50%" };

  return (
    <div className={"globe" + (dragging ? " dragging" : "") + (hover ? " zoomed" : "")}
    onPointerDown={onPointerDown}
    onPointerMove={onPointerMove}
    onPointerUp={onPointerUp}
    onPointerCancel={onPointerUp}>
      <svg viewBox="0 0 400 400" className="globe-svg" style={zoomOriginStyle}>
        <defs>
          <radialGradient id="globeShade" cx="38%" cy="30%" r="75%">
            <stop offset="0%" stopColor="#dfeaf1" />
            <stop offset="50%" stopColor="#bcd0dc" />
            <stop offset="85%" stopColor="#92a9b7" />
            <stop offset="100%" stopColor="#6d8492" />
          </radialGradient>
          <radialGradient id="globeAtmo" cx="50%" cy="50%" r="56%">
            <stop offset="78%" stopColor="rgba(160,200,230,0)" />
            <stop offset="92%" stopColor="rgba(160,200,230,0.35)" />
            <stop offset="100%" stopColor="rgba(160,200,230,0)" />
          </radialGradient>
          <radialGradient id="globeGloss" cx="32%" cy="22%" r="44%">
            <stop offset="0%" stopColor="rgba(255,255,255,0.55)" />
            <stop offset="60%" stopColor="rgba(255,255,255,0.06)" />
            <stop offset="100%" stopColor="rgba(255,255,255,0)" />
          </radialGradient>
          <clipPath id="globeClip">
            <circle cx={GLOBE_CX} cy={GLOBE_CY} r={GLOBE_R} />
          </clipPath>
        </defs>

        {/* atmosphere */}
        <circle cx={GLOBE_CX} cy={GLOBE_CY} r={GLOBE_R + 14} fill="url(#globeAtmo)" />

        {/* ocean */}
        <circle cx={GLOBE_CX} cy={GLOBE_CY} r={GLOBE_R} fill="url(#globeShade)" />

        <g clipPath="url(#globeClip)" className="globe-inner">
          {/* graticule */}
          <path d={pathGen(graticule()) || ""} className="globe-grid-line" />
          {/* equator + prime meridian */}
          <path d={pathGen({ type: "LineString", coordinates: [[-180, 0], [-90, 0], [0, 0], [90, 0], [180, 0]] }) || ""} className="globe-grid-line equator" />

          {/* land — plain fill, no per-country highlight */}
          {features && features.map((c, i) => {
            const d = pathGen(c);
            if (!d) return null;
            return <path key={i} d={d} className="country" />;
          })}
        </g>

        {/* highlight gloss */}
        <circle cx={GLOBE_CX} cy={GLOBE_CY} r={GLOBE_R} fill="url(#globeGloss)" pointerEvents="none" />

        {/* outline */}
        <circle cx={GLOBE_CX} cy={GLOBE_CY} r={GLOBE_R} fill="none" stroke="#5a6f7d" strokeWidth="0.6" strokeOpacity="0.5" pointerEvents="none" />

        {/* pins */}
        {pins.map((p) => {
          if (!p.proj.visible) return null;
          const fade = Math.max(0.3, 1 - p.proj.dist / (Math.PI / 2) * 0.7);
          const isHover = hover === p.city;
          const isLived = p.kind === "lived";
          const showMini = labelable[p.city];
          const labelDx = p.proj.x > GLOBE_CX ? 6 : -6;
          const labelAnchor = p.proj.x > GLOBE_CX ? "start" : "end";
          const pinKey = `${p.country}-${p.city}`;
          return (
            <g key={pinKey} style={{ opacity: fade }}>
              {isHover &&
              <circle cx={p.proj.x} cy={p.proj.y} r="9" className="globe-pin-halo">
                  <animate attributeName="r" values="6;16;6" dur="1.8s" repeatCount="indefinite" />
                  <animate attributeName="opacity" values="0.4;0;0.4" dur="1.8s" repeatCount="indefinite" />
                </circle>
              }
              {/* lived-in cities get an outer ring */}
              {isLived &&
              <circle
                cx={p.proj.x} cy={p.proj.y}
                r={isHover ? 7 : 5.6}
                fill="none"
                stroke="var(--accent)"
                strokeWidth="1.2" />

              }
              <circle
                cx={p.proj.x} cy={p.proj.y}
                r={isHover ? 3.6 : isLived ? 2.8 : 2.4}
                className="globe-pin-core"
                style={{
                  fill: isLived ? "var(--accent)" : "#0e0e0e",
                  stroke: "#fff",
                  strokeWidth: 0.7
                }} />
              
              {showMini &&
              <text
                className="globe-pin-label-mini"
                x={p.proj.x + labelDx}
                y={p.proj.y - 8}
                textAnchor={labelAnchor}>
                {p.city}</text>
              }
              <circle
                cx={p.proj.x} cy={p.proj.y} r="14"
                fill="transparent"
                style={{ cursor: "pointer" }}
                onMouseEnter={() => setHover(p.city)}
                onMouseLeave={() => setHover(null)} />
              
            </g>);

        })}

        {/* hover label */}
        {hover && (() => {
          const p = pins.find((x) => x.city === hover);
          if (!p || !p.proj.visible) return null;
          const label = p.city;
          const w = label.length * 5.8 + 16;
          const lx = p.proj.x + 8;
          const ly = p.proj.y - 12;
          const flip = lx + w > 400;
          const labelX = flip ? p.proj.x - 8 - w : lx;
          return (
            <g pointerEvents="none">
              <line x1={p.proj.x} y1={p.proj.y} x2={flip ? labelX + w : labelX} y2={ly + 7} stroke="#fff" strokeWidth="0.6" />
              <rect x={labelX} y={ly - 4} width={w} height="16" rx="3" className="globe-pin-label-bg" />
              <text x={labelX + w / 2} y={ly + 7} textAnchor="middle" className="globe-pin-label">{label}</text>
            </g>);

        })()}
      </svg>

      <div className="globe-readout"><span className="now">{readout}</span></div>
    </div>);

}

function GlobeSection() {
  return (
    <section className="section globe-section" id="places" data-screen-label="places">
      <SecHead title="Hobbies" count={PLACES.length} />
      <div className="globe-grid-wrap">
        <div className="globe-blurb">
          <h2>Travelling</h2>
          <p>
            I’ll try to keep this updated.
          </p>
          <div className="legend-row">
            <span className="legend-key"><span className="legend-dot lived"></span> lived</span>
            <span className="legend-key"><span className="legend-dot travel"></span> visited</span>
          </div>
          <div className="stat-grid">
            <div className="stat">
              <div className="n">{PLACES.length}</div>
              <div className="l">cities</div>
            </div>
            <div className="stat">
              <div className="n">{COUNTRY_COUNT}</div>
              <div className="l">countries</div>
            </div>
          </div>
        </div>
        <div className="globe-stage">
          <Globe />
        </div>
      </div>
    </section>);

}

window.Globe = Globe;
window.GlobeSection = GlobeSection;