/* Shared Europe map component.
   Renders projected country paths from world-atlas topojson via d3-geo.
   Each design styles fills, strokes, labels independently via props.
*/

function useEuTopo() {
  const [topo, setTopo] = React.useState(window.EU_TOPO || null);
  React.useEffect(() => {
    if (window.EU_TOPO) { setTopo(window.EU_TOPO); return; }
    const h = () => setTopo(window.EU_TOPO);
    window.addEventListener('eu-topo-ready', h);
    return () => window.removeEventListener('eu-topo-ready', h);
  }, []);
  return topo;
}

// Memoised projection + path builder for a given size & feature collection.
function useEuropePaths(width, height, padding, extent) {
  const topo = useEuTopo();
  return React.useMemo(() => {
    if (!topo) return null;
    const all = topojson.feature(topo, topo.objects.countries).features;
    // Crop to a Europe-focused bbox by centroid (keeps countries we want to draw)
    const inBox = all.filter(f => {
      const c = d3.geoCentroid(f);
      if (!c || isNaN(c[0])) return false;
      return c[0] > -25 && c[0] < 42 && c[1] > 33 && c[1] < 71;
    });

    // Fit projection to a fixed bbox over Europe. d3-geo treats GeoJSON
    // polygons with the "left-hand rule" (exterior ring clockwise on the
    // sphere) — we provide the winding accordingly so geoBounds returns the
    // small Europe extent, not the entire sphere.
    const bbox = {
      type: 'Polygon',
      coordinates: [[
        [-12, 34], [-12, 71], [40, 71], [40, 34], [-12, 34],
      ]],
    };
    const fitExtent = extent || [
      [padding, padding],
      [width - padding, height - padding],
    ];
    const proj = d3.geoMercator().fitExtent(fitExtent, bbox);
    const path = d3.geoPath(proj);
    const features = inBox.map(f => {
      const iso = window.ISO_NUM_TO_3[String(f.id).padStart(3, '0')];
      const country = iso ? window.EU_COUNTRIES.find(c => c.iso === iso) : null;
      const d = path(f);
      const c = proj(d3.geoCentroid(f));
      return { f, iso, country, d, cx: c && c[0], cy: c && c[1], isEU: !!country };
    });
    return { features, proj, path };
  }, [topo, width, height, padding, extent && extent.join(',')]);
}

function EuropeMap({
  width,
  height,
  padding = 20,
  extent,
  background = 'transparent',
  oceanColor = null,
  otherFill = '#e5e7eb',
  otherStroke = '#cbd5e1',
  euFill,           // (country, max) => color
  euStroke = '#ffffff',
  euStrokeWidth = 0.6,
  selectedIso,
  onSelect,
  hoveredIso,
  onHover,
  showLabels = false,
  labelColor = '#1f2937',
  labelMinSpend = 30,
  showValues = false,
  valueColor = '#ffffff',
  showDots = false,
  dotForCountry,    // (country) => {r, fill, stroke}
  graticule = false,
  interactive = false, // enable wheel-zoom + drag-pan
  children,
}) {
  const build = useEuropePaths(width, height, padding, extent);
  const max = React.useMemo(
    () => Math.max(...window.EU_COUNTRIES.map(c => c.spend)),
    []
  );

  // Pan/zoom state — applied as a transform on the inner group
  const [tx, setTx] = React.useState(0);
  const [ty, setTy] = React.useState(0);
  const [zoom, setZoom] = React.useState(1);
  const [isDragging, setIsDragging] = React.useState(false);
  const svgRef = React.useRef(null);
  const dragRef = React.useRef(null);  // { startX, startY, startTx, startTy, moved }

  // Clamp pan so the map can't drift off the canvas. Allowed slack grows with
  // zoom (at 1× we lock to ~10% slack, at 6× we let the user travel further).
  const clampPan = (nx, ny, z = zoom) => {
    const slack = Math.max(0.1, z - 0.5);
    const maxX = width * slack;
    const maxY = height * slack;
    return [
      Math.max(-maxX, Math.min(maxX, nx)),
      Math.max(-maxY, Math.min(maxY, ny)),
    ];
  };

  // Wheel zoom — bind at window level and gate by hit-testing the SVG rect.
  // We bind to window (not the SVG) because empty SVG areas can't be reliably
  // hit-tested for wheel events across browsers; window listener always fires.
  React.useEffect(() => {
    if (!interactive) return;
    const onWheel = (e) => {
      const svg = svgRef.current;
      if (!svg) return;
      const rect = svg.getBoundingClientRect();
      // Only act when the cursor is over the SVG bounds.
      if (
        e.clientX < rect.left || e.clientX > rect.right ||
        e.clientY < rect.top || e.clientY > rect.bottom
      ) return;
      // Skip when cursor is over a UI panel sitting above the map.
      const overPanel = (e.target instanceof Element) && e.target.closest(
        '.ps-panel, .ps-stats-widget, .ps-brand, .ps-legend, .ps-deal-modal'
      );
      if (overPanel) return;
      e.preventDefault();
      const cssX = e.clientX - rect.left;
      const cssY = e.clientY - rect.top;
      const userX = cssX * (width / rect.width);
      const userY = cssY * (height / rect.height);
      const factor = Math.exp(-e.deltaY * 0.0015);
      setZoom((z) => {
        const next = Math.max(0.7, Math.min(6, z * factor));
        const realFactor = next / z;
        const slack = Math.max(0.1, next - 0.5);
        setTx((curTx) => {
          const cand = userX - (userX - curTx) * realFactor;
          return Math.max(-width * slack, Math.min(width * slack, cand));
        });
        setTy((curTy) => {
          const cand = userY - (userY - curTy) * realFactor;
          return Math.max(-height * slack, Math.min(height * slack, cand));
        });
        return next;
      });
    };
    window.addEventListener('wheel', onWheel, { passive: false });
    return () => window.removeEventListener('wheel', onWheel);
  }, [interactive, width, height]);

  // Pointer drag handlers — only capture/treat as drag AFTER a 4px move.
  // Until then, clicks on countries (and hover events) fire normally.
  const onPointerDown = (e) => {
    if (!interactive) return;
    dragRef.current = {
      startX: e.clientX,
      startY: e.clientY,
      startTx: tx, startTy: ty,
      moved: false,
      captured: false,
      pointerId: e.pointerId,
      target: e.currentTarget,
    };
  };
  const onPointerMove = (e) => {
    const d = dragRef.current;
    if (!d) return;
    const dx = e.clientX - d.startX;
    const dy = e.clientY - d.startY;
    if (!d.moved && (Math.abs(dx) > 4 || Math.abs(dy) > 4)) {
      d.moved = true;
      setIsDragging(true);
      // Capture pointer only now — too late to affect the click we won't
      // be firing, but keeps the drag smooth even if the cursor strays
      // outside the SVG bounds.
      try { d.target.setPointerCapture(d.pointerId); d.captured = true; } catch (_) {}
    }
    if (d.moved) {
      const rect = svgRef.current.getBoundingClientRect();
      const scaleX = width / rect.width;
      const scaleY = height / rect.height;
      const [cx, cy] = clampPan(d.startTx + dx * scaleX, d.startTy + dy * scaleY);
      setTx(cx);
      setTy(cy);
    }
  };
  const onPointerUp = (e) => {
    const d = dragRef.current;
    if (d && d.captured) {
      try { d.target.releasePointerCapture(d.pointerId); } catch (_) {}
    }
    dragRef.current = null;
    setIsDragging(false);
  };
  const wasDragged = () => dragRef.current && dragRef.current.moved;

  if (!build) {
    return (
      <div style={{
        width, height,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        color: '#9ca3af', fontSize: 12, fontFamily: 'monospace',
        background,
      }}>Loading map…</div>
    );
  }

  const transform = `translate(${tx} ${ty}) scale(${zoom})`;

  return (
    <svg
      ref={svgRef}
      width={width} height={height} viewBox={`0 0 ${width} ${height}`}
      style={{
        display: 'block', background, overflow: 'visible',
        cursor: interactive ? (isDragging ? 'grabbing' : 'grab') : 'default',
        userSelect: 'none',
        WebkitUserSelect: 'none',
        touchAction: interactive ? 'none' : 'auto',
      }}
      onPointerDown={onPointerDown}
      onPointerMove={onPointerMove}
      onPointerUp={onPointerUp}
      onPointerCancel={onPointerUp}>
      
      {oceanColor && <rect width={width} height={height} fill={oceanColor} />}
      {/* Transparent hit-rect so wheel & drag fire on empty (no-country) areas */}
      {!oceanColor && interactive && (
        <rect
          width={width} height={height}
          fill="transparent"
          pointerEvents="all"
          style={{ cursor: isDragging ? 'grabbing' : 'grab' }} />
      )}

      <g transform={transform}>
      {graticule && (
        <path
          d={d3.geoPath(build.proj)(d3.geoGraticule().step([10, 10])())}
          fill="none"
          stroke="rgba(148, 163, 184, 0.15)"
          strokeWidth="0.5"
        />
      )}

      {/* Non-EU countries (context) */}
      <g>
        {build.features.filter(x => !x.isEU).map((x, i) => (
          <path
            key={'o' + i}
            d={x.d}
            fill={otherFill}
            stroke={otherStroke}
            strokeWidth="0.5"
          />
        ))}
      </g>

      {/* EU countries */}
      <g>
        {build.features.filter(x => x.isEU).map((x, i) => {
          const isSel = selectedIso === x.iso;
          const isHov = hoveredIso === x.iso;
          const fill = typeof euFill === 'function' ? euFill(x.country, max) : euFill;
          return (
            <path
              key={x.iso}
              d={x.d}
              fill={fill}
              stroke={isSel ? '#111827' : euStroke}
              strokeWidth={isSel ? 1.6 : (isHov ? 1.2 : euStrokeWidth)}
              style={{ cursor: onSelect ? 'pointer' : 'default', transition: 'opacity 120ms' }}
              opacity={hoveredIso && !isHov && !isSel ? 0.85 : 1}
              onMouseEnter={onHover ? () => onHover(x.iso) : undefined}
              onMouseLeave={onHover ? () => onHover(null) : undefined}
              onClick={onSelect ? (e) => { if (!wasDragged()) onSelect(x.iso); } : undefined}
            />
          );
        })}
      </g>

      {/* Labels */}
      {showLabels && (
        <g style={{ pointerEvents: 'none' }}>
          {build.features.filter(x => x.isEU && x.country.spend >= labelMinSpend).map(x => (
            <g key={'l' + x.iso}>
              <text
                x={x.cx}
                y={x.cy - (showValues ? 3 : 0)}
                textAnchor="middle"
                fontSize="11"
                fontWeight="600"
                fontFamily="Inter, sans-serif"
                fill={labelColor}
              >{x.country.name}</text>
              {showValues && (
                <text
                  x={x.cx}
                  y={x.cy + 11}
                  textAnchor="middle"
                  fontSize="10"
                  fontFamily="'JetBrains Mono', monospace"
                  fill={labelColor}
                  opacity="0.85"
                >€{x.country.spend.toFixed(1)}B</text>
              )}
            </g>
          ))}
        </g>
      )}

      {/* Optional dots overlay (for buyer scatter etc) */}
      {showDots && (
        <g style={{ pointerEvents: 'none' }}>
          {build.features.filter(x => x.isEU).map(x => {
            const dot = dotForCountry && dotForCountry(x.country);
            if (!dot) return null;
            return (
              <g key={'d' + x.iso}>
                <circle cx={x.cx} cy={x.cy} r={dot.r} fill={dot.fill} stroke={dot.stroke || 'none'} strokeWidth={dot.strokeWidth || 0} />
              </g>
            );
          })}
        </g>
      )}

      {/* Children rendered on top — for arrows / overlays. Children receive
          { ...build, zoom } so they can counter-scale text to stay crisp. */}
      {typeof children === 'function' ? children({ ...build, zoom }) : children}

      </g>
    </svg>
  );
}

// Helper to get the projected pixel location of a country centroid.
function getEuCentroid(build, iso) {
  const f = build.features.find(x => x.iso === iso);
  return f ? [f.cx, f.cy] : null;
}

Object.assign(window, { EuropeMap, getEuCentroid });
