#!/usr/bin/env python3
"""Extract red land polygons from Mercator reference image -> layered SVG paths."""

from __future__ import annotations

import json
import math
from collections import deque
from pathlib import Path

import numpy as np
from PIL import Image

ROOT = Path(__file__).resolve().parent
SRC = ROOT / "mercator-source.jpg"
OUT_FLAT = ROOT / "hub-logo-continents-mercator-flat.svg"
OUT_GLOBE = ROOT / "hub-logo-continents-mercator-globe.svg"
OUT_GLOBE_SNAP_VERTICAL = ROOT / "hub-logo-continents-mercator-globe.snapshot-vertical.svg"
OUT_META = ROOT / "continents-meta.json"

# Globe framing in 1920x1080 logo scene.
GLOBE_CX = 720.0
GLOBE_CY = 560.0
GLOBE_R = 322.0
# Orthographic center (visible hemisphere).
CENTER_LAT = 18.0
CENTER_LON = 12.0


def red_mask(rgb: np.ndarray) -> np.ndarray:
    r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2]
    return (r.astype(np.int16) > 150) & (g.astype(np.int16) < 120) & (b.astype(np.int16) < 120)


def dilate(mask: np.ndarray, radius: int = 3) -> np.ndarray:
    h, w = mask.shape
    out = mask.copy()
    ys, xs = np.where(mask)
    for y, x in zip(ys, xs):
        y0 = max(0, y - radius)
        y1 = min(h, y + radius + 1)
        x0 = max(0, x - radius)
        x1 = min(w, x + radius + 1)
        if mask[y0:y1, x0:x1].any():
            out[y0:y1, x0:x1] = True
    return out


def label_components(mask: np.ndarray) -> tuple[np.ndarray, int]:
    h, w = mask.shape
    labels = np.zeros((h, w), dtype=np.int32)
    current = 0
    for y in range(h):
        for x in range(w):
            if not mask[y, x] or labels[y, x]:
                continue
            current += 1
            q = deque([(x, y)])
            labels[y, x] = current
            while q:
                cx, cy = q.popleft()
                for nx, ny in ((cx + 1, cy), (cx - 1, cy), (cx, cy + 1), (cx, cy - 1)):
                    if 0 <= nx < w and 0 <= ny < h and mask[ny, nx] and labels[ny, nx] == 0:
                        labels[ny, nx] = current
                        q.append((nx, ny))
    return labels, current


def trace_boundary(labels: np.ndarray, label_id: int) -> list[tuple[int, int]]:
    h, w = labels.shape
    comp = labels == label_id
    if not comp.any():
        return []

    ys, xs = np.where(comp)
    start = (int(xs[0]), int(ys[0]))
    x, y = start
    path = [start]
    dirs = [(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)]
    dir_idx = 0

    def is_land(px: int, py: int) -> bool:
        return 0 <= px < w and 0 <= py < h and labels[py, px] == label_id

    for _ in range(w * h * 4):
        found = False
        for i in range(8):
            nd = (dir_idx + i) % 8
            dx, dy = dirs[nd]
            nx, ny = x + dx, y + dy
            if is_land(nx, ny):
                x, y = nx, ny
                dir_idx = (nd + 6) % 8
                path.append((x, y))
                found = True
                break
        if not found:
            break
        if (x, y) == start and len(path) > 8:
            break
    return path


def simplify(points: list[tuple[int, int]], min_dist: float = 2.0) -> list[tuple[int, int]]:
    if len(points) < 3:
        return points
    out = [points[0]]
    for p in points[1:]:
        ox, oy = out[-1]
        if math.hypot(p[0] - ox, p[1] - oy) >= min_dist:
            out.append(p)
    if out[0] != out[-1]:
        out.append(out[0])
    return out


def path_d(points: list[tuple[float, float]]) -> str:
    if len(points) < 3:
        return ""
    x0, y0 = points[0]
    parts = [f"M {x0:.1f} {y0:.1f}"]
    for x, y in points[1:]:
        parts.append(f"L {x:.1f} {y:.1f}")
    parts.append("Z")
    return " ".join(parts)


def mercator_y_to_lat(y: float, h: float) -> float:
    t = 1.0 - (y / h)
    t = min(max(t, 0.02), 0.98)
    return math.degrees(math.atan(math.sinh(math.pi * (2 * t - 1))))


def lat_lon_to_globe(lat: float, lon: float) -> tuple[float, float]:
    lat_r = math.radians(lat)
    lon_r = math.radians(lon)
    lon0 = math.radians(CENTER_LON)
    dlon = lon_r - lon0
    x = GLOBE_CX + GLOBE_R * math.cos(lat_r) * math.sin(dlon)
    y = GLOBE_CY - GLOBE_R * math.sin(lat_r)
    return x, y


def map_to_globe(points: list[tuple[int, int]], w: int, h: int) -> str:
    mapped: list[tuple[float, float]] = []
    for x, y in points:
        lon = (x / w) * 360.0 - 180.0
        lat = mercator_y_to_lat(float(y), float(h))
        mapped.append(lat_lon_to_globe(lat, lon))
    if len(mapped) < 3:
        return ""
    return path_d(mapped)


def main() -> None:
    img = Image.open(SRC).convert("RGB")
    rgb = np.array(img)
    w, h = rgb.shape[1], rgb.shape[0]
    mask = dilate(red_mask(rgb), radius=3)
    labels, n = label_components(mask)

    components: list[dict] = []
    for label_id in range(1, n + 1):
        area = int((labels == label_id).sum())
        if area < 500:
            continue
        boundary = trace_boundary(labels, label_id)
        if len(boundary) < 20:
            continue
        boundary = simplify(boundary, min_dist=3.5)
        flat = path_d([(float(x), float(y)) for x, y in boundary])
        globe = map_to_globe(boundary, w, h)
        if not flat:
            continue
        ys, xs = np.where(labels == label_id)
        components.append(
            {
                "id": label_id,
                "area": area,
                "bbox": [int(xs.min()), int(ys.min()), int(xs.max()), int(ys.max())],
                "flat": flat,
                "globe": globe,
            }
        )

    components.sort(key=lambda c: c["area"], reverse=True)

    flat_paths = "\n      ".join(
        f'<path id="continent-{c["id"]}" data-area="{c["area"]}" d="{c["flat"]}"/>'
        for c in components
    )
    globe_paths = "\n        ".join(
        f'<path id="continent-{c["id"]}" data-area="{c["area"]}" d="{c["globe"]}"/>'
        for c in components
    )

    OUT_FLAT.write_text(
        f"""<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}" role="img" aria-label="Extracted continents (flat Mercator plate)">
  <rect width="100%" height="100%" fill="#b8d4e8"/>
  <g id="continents-inner-layer" fill="#9ab3cc" fill-opacity="0.82" stroke="#dce8f6" stroke-opacity="0.78" stroke-width="1.1">
    {flat_paths}
  </g>
</svg>
""",
        encoding="utf-8",
    )

    # Two-step alignment:
    # 1) Vertical fit: Mercator north/south poles match globe top/bottom.
    # 2) Horizontal fit: layer centers/pivots match at globe center.
    globe_diameter = GLOBE_R * 2
    inner_sy = globe_diameter / h
    inner_sx = inner_sy
    inner_ty = GLOBE_CY - GLOBE_R
    inner_tx_vertical = 0.0
    inner_tx = GLOBE_CX - (w * inner_sx) / 2
    outer_sx = 1.0
    outer_sy = 1.0

    def globe_svg(
        inner_tx_val: float,
        inner_ty_val: float,
        inner_sx_val: float,
        inner_sy_val: float,
        outer_sx_val: float,
        outer_sy_val: float,
        label: str,
    ) -> str:
        return f"""<svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1080" viewBox="0 0 1920 1080" preserveAspectRatio="xMidYMid slice" role="img" aria-label="{label}">
  <defs>
    <clipPath id="globeClip"><circle cx="{GLOBE_CX:.0f}" cy="{GLOBE_CY:.0f}" r="{GLOBE_R:.0f}"/></clipPath>
    <radialGradient id="continentRelief" cx="34%" cy="22%" r="78%">
      <stop offset="0%" stop-color="#dbe7f6" stop-opacity="0.45"/>
      <stop offset="58%" stop-color="#9ab3cc" stop-opacity="0.22"/>
      <stop offset="100%" stop-color="#6a7f97" stop-opacity="0.18"/>
    </radialGradient>
    <filter id="continentDepth" x="-30%" y="-30%" width="160%" height="160%">
      <feDropShadow dx="-4" dy="-6" stdDeviation="3" flood-color="#f4fbff" flood-opacity="0.2"/>
      <feDropShadow dx="4" dy="6" stdDeviation="4" flood-color="#0a1220" flood-opacity="0.28"/>
    </filter>
  </defs>
  <g id="continents-outer-layer" clip-path="url(#globeClip)" transform="translate({GLOBE_CX:.0f} {GLOBE_CY:.0f}) scale({outer_sx_val:.3f} {outer_sy_val:.3f}) translate(-{GLOBE_CX:.0f} -{GLOBE_CY:.0f})">
    <g id="continents-inner-layer" transform="translate({inner_tx_val:.2f} {inner_ty_val:.2f}) scale({inner_sx_val:.4f} {inner_sy_val:.4f})">
      <g id="continents-flat" fill="url(#continentRelief)" fill-opacity="0.9" stroke="#dce8f6" stroke-opacity="0.78" stroke-width="1.2" filter="url(#continentDepth)">
        {flat_paths}
      </g>
    </g>
  </g>
</svg>
"""

    OUT_GLOBE_SNAP_VERTICAL.write_text(
        globe_svg(
            inner_tx_vertical,
            inner_ty,
            inner_sx,
            inner_sy,
            outer_sx,
            outer_sy,
            "Continents vertical-fit snapshot",
        ),
        encoding="utf-8",
    )

    OUT_GLOBE.write_text(
        globe_svg(
            inner_tx,
            inner_ty,
            inner_sx,
            inner_sy,
            outer_sx,
            outer_sy,
            "Continents layered Mercator overlay",
        ),
        encoding="utf-8",
    )

    OUT_META.write_text(
        json.dumps(
            {
                "source": str(SRC.name),
                "size": [w, h],
                "components": len(components),
                "layers": {
                    "vertical_snapshot": {
                        "translate": [inner_tx_vertical, inner_ty],
                        "scale": [inner_sx, inner_sy],
                    },
                    "inner": {"translate": [inner_tx, inner_ty], "scale": [inner_sx, inner_sy]},
                    "outer": {"scale": [outer_sx, outer_sy]},
                    "pivot": [GLOBE_CX, GLOBE_CY],
                },
                "areas_top10": [c["area"] for c in components[:10]],
            },
            indent=2,
        ),
        encoding="utf-8",
    )

    print(f"components={len(components)}")
    print(f"wrote {OUT_FLAT}")
    print(f"wrote {OUT_GLOBE}")


if __name__ == "__main__":
    main()
