#!/usr/bin/env python3
"""
Convergence Archipelago — Beautiful Parchment-Style Nautical Map Generator
=========================================================================
Produces a hand-drawn antique fantasy nautical chart:
- Parchment texture with stains, edge burn, fold creases
- Ocean with depth gradient, wave lines, rhumb lines, sea monsters
- Irregular islands with coastal shelves, mountains, trees, rivers
- Watercolor faction territories with hand-drawn boundaries
- Ornate compass roses, scroll cartouches, corner motifs
- Dashed routes with ship icons, treasure marks, anomaly zones

Requirements: pip install pillow numpy scipy
"""

import math
import os
import random
import numpy as np
from PIL import Image, ImageDraw, ImageFont

# ══════════════════════════════════════════════════════════════════════
#  CONFIGURATION & DATA
# ══════════════════════════════════════════════════════════════════════
SEED = 42
random.seed(SEED)
np.random.seed(SEED)

CANVAS_W, CANVAS_H = 2400, 1600
COORD_MAX = 1000


def cx(x: float) -> int:
    return int(x / COORD_MAX * CANVAS_W)


def cy(y: float) -> int:
    return int(y / COORD_MAX * CANVAS_H)


def cs(s: float) -> int:
    return int(s / COORD_MAX * CANVAS_W)


# ── Factions ────────────────────────────────────────────────────────
FACTIONS = {
    "Reef Lords":    {"color": "#c0392b", "hex": (139, 58, 58)},
    "Silver Navy":   {"color": "#95a5a6", "hex": (141, 141, 141)},
    "The Displaced": {"color": "#8e44ad", "hex": (123, 94, 158)},
    "The Harrowed":  {"color": "#2c3e50", "hex": (44, 62, 80)},
    "Free Ports":    {"color": "#27ae60", "hex": (93, 138, 76)},
}

# ── Locations ───────────────────────────────────────────────────────
LOCATIONS = [
    # Central Hub
    {"name": "Port Veridian",         "type": "capital",   "faction": "Free Ports",    "x": 520, "y": 480, "island_r": 55, "label": True},
    {"name": "Vault of Tides",        "type": "landmark",  "faction": "Free Ports",    "x": 545, "y": 455, "island_r": 18, "label": True},
    {"name": "Salt Temple",           "type": "town",      "faction": "Free Ports",    "x": 475, "y": 440, "island_r": 22, "label": True},
    {"name": "The Salt-Circle",       "type": "ritual",    "faction": "Free Ports",    "x": 455, "y": 420, "island_r": 14, "label": True},
    {"name": "Gallows Docks",         "type": "gallows",   "faction": "Reef Lords",    "x": 565, "y": 520, "island_r": 16, "label": True},
    {"name": "Drowned Anchor",        "type": "town",      "faction": "Free Ports",    "x": 435, "y": 545, "island_r": 20, "label": True},
    {"name": "Dragon Turtle",         "type": "mobile",    "faction": "Free Ports",    "x": 500, "y": 565, "island_r": 22, "label": True},
    {"name": "The Bone Cathedral",    "type": "underwater","faction": "The Harrowed",  "x": 415, "y": 500, "island_r": 0,  "label": True},
    # Northern
    {"name": "Singing Reefs",         "type": "hazard",    "faction": "Silver Navy",   "x": 480, "y": 175, "island_r": 18, "label": True},
    {"name": "Silver Outpost",        "type": "town",      "faction": "Silver Navy",   "x": 525, "y": 155, "island_r": 28, "label": True},
    # Northeastern
    {"name": "Pearl Banks",           "type": "region",    "faction": "Reef Lords",    "x": 720, "y": 195, "island_r": 30, "label": True},
    {"name": "Sahuagin Temple-City",  "type": "underwater","faction": "Reef Lords",    "x": 785, "y": 240, "island_r": 0,  "label": True},
    {"name": "Shattered Reefs",       "type": "hazard",    "faction": "Reef Lords",    "x": 750, "y": 305, "island_r": 35, "label": True},
    {"name": "Storm-Lanes",           "type": "region",    "faction": "Reef Lords",    "x": 825, "y": 280, "island_r": 25, "label": True},
    # Southeastern
    {"name": "The Flotilla",          "type": "town",      "faction": "Free Ports",    "x": 700, "y": 650, "island_r": 40, "label": True},
    # Southwestern
    {"name": "The Sargasso Maze",     "type": "hazard",    "faction": "The Displaced", "x": 245, "y": 700, "island_r": 45, "label": True},
    {"name": "Bone Forest",           "type": "landmark",  "faction": "The Harrowed",  "x": 215, "y": 745, "island_r": 20, "label": True},
    {"name": "The Arch",              "type": "landmark",  "faction": "The Displaced", "x": 285, "y": 725, "island_r": 12, "label": True},
    {"name": "Black Sand Beach",      "type": "beach",     "faction": "The Harrowed",  "x": 195, "y": 780, "island_r": 16, "label": True},
    # Western
    {"name": "Harrowed Hidden Temple","type": "temple",    "faction": "The Harrowed",  "x": 175, "y": 315, "island_r": 22, "label": True},
    {"name": "The Convergence Spire", "type": "spire",     "faction": "The Harrowed",  "x": 225, "y": 380, "island_r": 50, "label": True},
    {"name": "Storm Giant Citadel",   "type": "flying",    "faction": "Silver Navy",   "x": 305, "y": 245, "island_r": 0,  "label": True},
    {"name": "The Maze Below",        "type": "hazard",    "faction": "The Displaced", "x": 260, "y": 420, "island_r": 18, "label": False},
    # Open Ocean
    {"name": "Kraken's Lair",         "type": "monster",   "faction": "The Displaced", "x": 400, "y": 700, "island_r": 0,  "label": True},
    {"name": "The Black Reach",       "type": "finale",    "faction": "The Harrowed",  "x": 140, "y": 830, "island_r": 30, "label": True},
    {"name": "Displaced Flotilla",    "type": "hazard",    "faction": "The Displaced", "x": 180, "y": 660, "island_r": 22, "label": False},
    {"name": "The Gutter",            "type": "slum",      "faction": "Free Ports",    "x": 460, "y": 510, "island_r": 10, "label": False},
]

# ── Treasure / Wreck Marks ─────────────────────────────────────────
TREASURE_MARKS = [
    (310, 500),   # The Black Iron
    (550, 620),   # The Hollow Crown
    (200, 640),   # The Aurora
    (160, 710),   # The First Loss
    (680, 380),   # Reef Lord Cache
    (580, 420),   # Sunken Galleon
]

# ── Magical Anomaly Zones ─────────────────────────────────────────
ANOMALY_ZONES = [
    {"x": 320, "y": 220, "r": 40, "label": "Convergence Storm"},
    {"x": 220, "y": 550, "r": 35, "label": "Convergence Storm"},
    {"x": 780, "y": 420, "r": 30, "label": "Convergence Storm"},
    {"x": 180, "y": 400, "r": 25, "label": "Dead Zone"},
    {"x": 350, "y": 620, "r": 25, "label": "Dead Zone"},
]

# ── Travel Routes ─────────────────────────────────────────────────
ROUTES = [
    ("Port Veridian", "The Flotilla"),
    ("Port Veridian", "Salt Temple"),
    ("Port Veridian", "Drowned Anchor"),
    ("Port Veridian", "Silver Outpost"),
    ("Port Veridian", "The Convergence Spire"),
    ("Port Veridian", "Gallows Docks"),
    ("Port Veridian", "Vault of Tides"),
    ("The Flotilla", "Shattered Reefs"),
    ("The Flotilla", "Storm-Lanes"),
    ("The Convergence Spire", "The Maze Below"),
    ("The Sargasso Maze", "Displaced Flotilla"),
    ("Silver Outpost", "Singing Reefs"),
]

# ── Faction Territory Blobs ───────────────────────────────────────
TERRITORIES = [
    {"name": "Reef Lords Territory",      "faction": "Reef Lords",    "x": 760, "y": 260, "rx": 150, "ry": 170},
    {"name": "Silver Navy Claim",         "faction": "Silver Navy",   "x": 500, "y": 160, "rx": 120, "ry": 110},
    {"name": "The Displaced Zone",        "faction": "The Displaced", "x": 240, "y": 680, "rx": 140, "ry": 150},
    {"name": "Harrowed Influence",        "faction": "The Harrowed",  "x": 200, "y": 380, "rx": 150, "ry": 180},
    {"name": "Free Ports",                "faction": "Free Ports",    "x": 520, "y": 500, "rx": 130, "ry": 120},
]

# ── Region Labels (text-only) ─────────────────────────────────────
REGION_LABELS = [
    {"text": "The Shifting",              "x": 230, "y": 640, "faction": "The Displaced"},
    {"text": "The Hollow",                "x": 190, "y": 350, "faction": "The Harrowed"},
    {"text": "Adm. Von Strangle",         "x": 540, "y": 140, "faction": "Silver Navy"},
    {"text": "Reef Lords Territory",      "x": 780, "y": 180, "faction": "Reef Lords"},
    {"text": "Harrowed Territory",        "x": 160, "y": 280, "faction": "The Harrowed"},
    {"text": "Displaced Wandering Zone",  "x": 200, "y": 620, "faction": "The Displaced"},
]

# ══════════════════════════════════════════════════════════════════════
#  PALETTE & FONTS
# ══════════════════════════════════════════════════════════════════════
PALETTE = {
    "parchment_light": (232, 213, 183),
    "parchment_dark":  (212, 184, 150),
    "parchment_shadow":(185, 160, 125),
    "ink":             (62, 39, 35),
    "ink_light":       (94, 66, 52),
    "ink_faint":       (140, 110, 85),
    "ocean_deep":      (52, 78, 92),
    "ocean_mid":       (72, 110, 128),
    "ocean_near":      (110, 150, 165),
    "coast_glow":      (78, 205, 196),
    "coast_glow_dark": (58, 170, 160),
    "hazard":          (160, 82, 45),
    "treasure":        (192, 57, 43),
}

FONT_CACHE = {}


def load_font(size: int, bold: bool = False, italic: bool = False) -> ImageFont.FreeTypeFont:
    key = (size, bold, italic)
    if key in FONT_CACHE:
        return FONT_CACHE[key]
    candidates = [
        "/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf",
        "/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf",
        "/usr/share/fonts/truetype/dejavu/DejaVuSerif-Italic.ttf",
        "/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf",
        "/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf",
        "/usr/share/fonts/truetype/liberation/LiberationSerif-Italic.ttf",
        "/usr/share/fonts/truetype/freefont/FreeSerifBold.ttf",
        "/usr/share/fonts/truetype/freefont/FreeSerif.ttf",
        "/usr/share/fonts/truetype/freefont/FreeSerifItalic.ttf",
        "/usr/share/fonts/truetype/noto/NotoSerif-Bold.ttf",
        "/usr/share/fonts/truetype/noto/NotoSerif-Regular.ttf",
        "/usr/share/fonts/truetype/noto/NotoSerif-Italic.ttf",
    ]
    if bold:
        order = [c for c in candidates if "Bold" in c] + candidates
    elif italic:
        order = [c for c in candidates if "Italic" in c] + candidates
    else:
        order = [c for c in candidates if ("Regular" in c and "Bold" not in c and "Italic" not in c)] + candidates
    for fp in order:
        if os.path.exists(fp):
            try:
                FONT_CACHE[key] = ImageFont.truetype(fp, size)
                return FONT_CACHE[key]
            except Exception:
                continue
    FONT_CACHE[key] = ImageFont.load_default()
    return FONT_CACHE[key]


def with_alpha(c, a):
    return (c[0], c[1], c[2], a)


# ══════════════════════════════════════════════════════════════════════
#  NOISE & TEXTURE
# ══════════════════════════════════════════════════════════════════════
def multi_octave_noise(h: int, w: int, seed: int = 42, octaves: int = 6, base_freq: int = 64) -> np.ndarray:
    np.random.seed(seed)
    result = np.zeros((h, w), dtype=np.float32)
    amp = 1.0
    for o in range(octaves):
        gh = max(2, h // (base_freq // (2 ** o)))
        gw = max(2, w // (base_freq // (2 ** o)))
        grid = np.random.rand(gh, gw).astype(np.float32)
        y_src = np.linspace(0, gh - 1, h)
        x_src = np.linspace(0, gw - 1, w)
        yi = np.clip(y_src.astype(np.int32), 0, gh - 1)
        xi = np.clip(x_src.astype(np.int32), 0, gw - 1)
        yi1 = np.clip(yi + 1, 0, gh - 1)
        xi1 = np.clip(xi + 1, 0, gw - 1)
        yf = (y_src - yi).astype(np.float32)
        xf = (x_src - xi).astype(np.float32)
        q11 = grid[yi[:, None], xi[None, :]]
        q21 = grid[yi[:, None], xi1[None, :]]
        q12 = grid[yi1[:, None], xi[None, :]]
        q22 = grid[yi1[:, None], xi1[None, :]]
        layer = (
            q11 * (1 - xf[None, :]) * (1 - yf[:, None]) +
            q21 * xf[None, :] * (1 - yf[:, None]) +
            q12 * (1 - xf[None, :]) * yf[:, None] +
            q22 * xf[None, :] * yf[:, None]
        )
        result += layer * amp
        amp *= 0.5
    result = (result - result.min()) / (result.max() - result.min() + 1e-8)
    return result


def make_parchment_layer(w: int, h: int, seed: int = 42) -> Image.Image:
    base1 = np.array(PALETTE["parchment_light"], dtype=np.float32)
    base2 = np.array(PALETTE["parchment_dark"], dtype=np.float32)
    n1 = multi_octave_noise(h, w, seed=seed, octaves=7, base_freq=128)
    n2 = multi_octave_noise(h, w, seed=seed + 1, octaves=4, base_freq=32)
    n3 = multi_octave_noise(h, w, seed=seed + 2, octaves=3, base_freq=16)

    arr = np.zeros((h, w, 4), dtype=np.uint8)
    for i in range(h):
        for j in range(w):
            t = n1[i, j] * 0.5 + n2[i, j] * 0.35 + n3[i, j] * 0.15
            c = base1 * t + base2 * (1 - t)
            dx = abs(j - w / 2) / (w / 2)
            dy = abs(i - h / 2) / (h / 2)
            edge = max(dx, dy)
            c *= (1 - edge ** 2.5 * 0.55)
            c[0] += n3[i, j] * 20
            c[1] += n2[i, j] * 8
            c[2] -= n3[i, j] * 5
            r, g, b = np.clip(c, 0, 255).astype(np.uint8)
            arr[i, j] = (r, g, b, 255)
    return Image.fromarray(arr, "RGBA")


def add_stain_rings(img: Image.Image, seed: int = 42, n_rings: int = 3) -> Image.Image:
    overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
    d = ImageDraw.Draw(overlay)
    w, h = img.size
    random.seed(seed)
    for _ in range(n_rings):
        x = random.randint(150, w - 150)
        y = random.randint(150, h - 150)
        r = random.randint(80, 180)
        color = (100, 80, 60, 25)
        for k in range(3):
            rr = r + k * 6
            d.ellipse([x - rr, y - rr, x + rr, y + rr], outline=color, width=2)
    return Image.alpha_composite(img, overlay)


def add_fold_creases(img: Image.Image) -> Image.Image:
    overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
    d = ImageDraw.Draw(overlay)
    w, h = img.size
    hy = h // 2
    d.line([(0, hy - 1), (w, hy - 1)], fill=(60, 45, 30, 15), width=2)
    d.line([(0, hy + 2), (w, hy + 2)], fill=(245, 230, 200, 12), width=2)
    vx = w // 2
    d.line([(vx - 1, 0), (vx - 1, h)], fill=(60, 45, 30, 15), width=2)
    d.line([(vx + 2, 0), (vx + 2, h)], fill=(245, 230, 200, 12), width=2)
    return Image.alpha_composite(img, overlay)


# ══════════════════════════════════════════════════════════════════════
#  OCEAN
# ══════════════════════════════════════════════════════════════════════
def make_ocean_layer(w: int, h: int, island_mask: np.ndarray, seed: int = 42) -> Image.Image:
    deep = np.array(PALETTE["ocean_deep"], dtype=np.float32)
    mid = np.array(PALETTE["ocean_mid"], dtype=np.float32)
    near = np.array(PALETTE["ocean_near"], dtype=np.float32)
    try:
        from scipy.ndimage import distance_transform_edt
        dist_to_land = distance_transform_edt(~island_mask)
        max_d = dist_to_land.max()
    except Exception:
        dist_to_land = np.zeros((h, w), dtype=np.float32)
        max_d = 1
    dnorm = dist_to_land / (max_d + 1e-8)
    n = multi_octave_noise(h, w, seed=seed + 10, octaves=5, base_freq=64)

    arr = np.zeros((h, w, 4), dtype=np.uint8)
    for i in range(h):
        for j in range(w):
            d = dnorm[i, j]
            if d < 0.08:
                base = near * 0.7 + mid * 0.3
            elif d < 0.25:
                t = (d - 0.08) / 0.17
                base = near * (1 - t) + mid * t
            elif d < 0.55:
                t = (d - 0.25) / 0.30
                base = mid * (1 - t) + deep * t
            else:
                base = deep
            wave = math.sin(j * 0.04 + i * 0.01 + n[i, j] * 2) * 0.5 + 0.5
            intensity = 0.92 + wave * 0.08 * (1 - d * 0.5)
            base = base * intensity
            r, g, b = np.clip(base, 0, 255).astype(np.uint8)
            arr[i, j] = (r, g, b, 255)
    return Image.fromarray(arr, "RGBA")


def add_wave_lines(img: Image.Image, island_mask: np.ndarray, seed: int = 42) -> Image.Image:
    overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
    d = ImageDraw.Draw(overlay)
    w, h = img.size
    random.seed(seed)
    for _ in range(45):
        y = random.randint(40, h - 40)
        x_start = random.randint(0, w // 3)
        x_end = x_start + random.randint(w // 3, w // 2)
        points = []
        for x in range(x_start, min(x_end, w), 8):
            yy = y + int(math.sin(x * 0.02 + random.random() * 3) * 3)
            if 0 <= yy < h and not island_mask[yy, x]:
                points.append((x, yy))
        if len(points) > 1:
            d.line(points, fill=(200, 220, 230, 20), width=1)
    return Image.alpha_composite(img, overlay)


def add_rhumb_lines(img: Image.Image, centers: list, island_mask: np.ndarray) -> Image.Image:
    overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
    d = ImageDraw.Draw(overlay)
    w, h = img.size
    for cx, cy in centers:
        for angle in range(0, 360, 10):
            rad = math.radians(angle)
            dx, dy = math.cos(rad), math.sin(rad)
            tmax = 1e9
            if dx > 0:
                tmax = min(tmax, (w - 1 - cx) / dx)
            elif dx < 0:
                tmax = min(tmax, -cx / dx)
            if dy > 0:
                tmax = min(tmax, (h - 1 - cy) / dy)
            elif dy < 0:
                tmax = min(tmax, -cy / dy)
            ex = int(cx + dx * tmax)
            ey = int(cy + dy * tmax)
            d.line([(cx, cy), (ex, ey)], fill=(210, 200, 180, 18), width=1)
    return Image.alpha_composite(img, overlay)


def add_sea_monsters(img: Image.Image, island_mask: np.ndarray) -> Image.Image:
    overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
    d = ImageDraw.Draw(overlay)
    w, h = img.size

    def serpent(pts):
        d.line(pts, fill=(80, 110, 120, 70), width=2)
        hx, hy = pts[-1]
        d.ellipse([hx - 6, hy - 6, hx + 6, hy + 6], fill=(80, 110, 120, 70))
        for i in range(3, len(pts) - 3, 4):
            x, y = pts[i]
            d.polygon([(x, y - 8), (x - 3, y + 2), (x + 3, y + 2)], fill=(80, 110, 120, 50))

    serpent_pts = []
    for x in range(1100, 1500, 15):
        y = 950 + int(math.sin(x * 0.02) * 40) + int(math.sin(x * 0.05) * 15)
        if 0 <= y < h and not island_mask[y, x]:
            serpent_pts.append((x, y))
    if serpent_pts:
        serpent(serpent_pts)

    wx, wy = 250, 180
    d.ellipse([wx - 35, wy - 12, wx + 45, wy + 18], fill=(70, 100, 110, 50), outline=(70, 100, 110, 70), width=1)
    d.polygon([(wx + 40, wy), (wx + 70, wy - 15), (wx + 65, wy + 10)], fill=(70, 100, 110, 50))
    d.ellipse([wx - 5, wy - 18, wx + 5, wy - 8], fill=(70, 100, 110, 50))
    d.arc([wx - 6, wy - 30, wx + 6, wy - 18], 180, 0, fill=(200, 220, 230, 40), width=2)

    tx, ty = 520, 900
    d.arc([tx - 20, ty - 30, tx + 20, ty + 20], 180, 0, fill=(100, 50, 90, 70), width=3)
    d.ellipse([tx - 6, ty - 36, tx + 6, ty - 24], fill=(100, 50, 90, 70))
    for dy, dx_off in [(-10, 0), (-2, 4), (6, 0)]:
        d.ellipse([tx - 3 + dx_off, ty + dy - 3, tx + 3 + dx_off, ty + dy + 3], fill=(140, 90, 130, 60))

    return Image.alpha_composite(img, overlay)


# ══════════════════════════════════════════════════════════════════════
#  ISLAND GEOMETRY
# ══════════════════════════════════════════════════════════════════════
def wobble_polygon(cx: int, cy: int, r: int, n: int = 30, irregularity: float = 0.35, wobble: float = 0.12) -> list:
    pts = []
    for i in range(n):
        angle = 2 * math.pi * i / n
        rr = r * (1.0 + random.uniform(-irregularity, irregularity))
        rr += r * wobble * math.sin(angle * 8 + random.random() * 10)
        if random.random() < 0.15:
            rr *= random.uniform(0.55, 1.45)
        x = cx + int(rr * math.cos(angle))
        y = cy + int(rr * math.sin(angle))
        pts.append((x, y))
    return pts


def point_in_polygon(x: int, y: int, poly: list) -> bool:
    inside = False
    n = len(poly)
    j = n - 1
    for i in range(n):
        xi, yi = poly[i]
        xj, yj = poly[j]
        if ((yi > y) != (yj > y)) and (x < (xj - xi) * (y - yi) / (yj - yi) + xi):
            inside = not inside
        j = i
    return inside


def polygon_bounds_mask(poly: list, w: int, h: int):
    xs = [p[0] for p in poly]
    ys = [p[1] for p in poly]
    minx = max(0, min(xs) - 2)
    maxx = min(w - 1, max(xs) + 2)
    miny = max(0, min(ys) - 2)
    maxy = min(h - 1, max(ys) + 2)
    mask = np.zeros((maxy - miny + 1, maxx - minx + 1), dtype=bool)
    for i in range(mask.shape[0]):
        for j in range(mask.shape[1]):
            mask[i, j] = point_in_polygon(minx + j, miny + i, poly)
    return mask, minx, miny


def draw_island_with_detail(draw: ImageDraw.ImageDraw, px: int, py: int, r: int, faction: str, ltype: str):
    poly = wobble_polygon(px, py, r, n=28, irregularity=0.32, wobble=0.12)
    # Shadow offset for pseudo-3D outline
    shadow_poly = [(x + 3, y + 4) for x, y in poly]
    draw.polygon(shadow_poly, fill=(140, 120, 90, 120))
    # Main island
    if ltype == "beach":
        fill = (62, 55, 48)
        inner_fill = (78, 68, 58)
    elif ltype == "finale":
        fill = (55, 45, 42)
        inner_fill = (65, 52, 48)
    else:
        fill = (210, 195, 150)
        inner_fill = (225, 210, 175)
    draw.polygon(poly, fill=fill, outline=(120, 100, 75, 255), width=2)

    if r > 12:
        inner_poly = wobble_polygon(px, py, int(r * 0.78), n=24, irregularity=0.20, wobble=0.08)
        draw.polygon(inner_poly, fill=inner_fill)

    # Mountains on large islands
    if r > 25:
        for _ in range(max(2, r // 14)):
            hx = px + random.randint(-r // 3, r // 3)
            hy = py + random.randint(-r // 3, r // 3)
            if point_in_polygon(hx, hy, poly):
                hr = max(4, r // 10)
                draw.polygon([(hx, hy - hr), (hx + hr, hy + hr // 2), (hx - hr, hy + hr // 2)],
                             fill=(130, 125, 105, 190), outline=(100, 90, 75, 150), width=1)

    # Trees on forested islands
    if faction in ["Free Ports", "The Harrowed"] and r > 15:
        for _ in range(max(3, r // 8)):
            tx = px + random.randint(-r // 2, r // 2)
            ty = py + random.randint(-r // 2, r // 2)
            if point_in_polygon(tx, ty, poly):
                draw.line([(tx, ty), (tx, ty + 5)], fill=(100, 80, 55, 180), width=1)
                draw.ellipse([tx - 4, ty - 6, tx + 4, ty + 2], fill=(70, 110, 55, 160),
                             outline=(50, 90, 45, 120), width=1)

    # River on very large islands
    if r > 45:
        river = []
        sx = px
        sy = py - r // 3
        if point_in_polygon(sx, sy, poly):
            for step in range(20):
                ry = sy + step * (r // 12)
                rx = sx + int(math.sin(step * 0.6) * (r // 6))
                if point_in_polygon(rx, ry, poly):
                    river.append((rx, ry))
            if len(river) > 1:
                draw.line(river, fill=(80, 120, 150, 160), width=2)
    return poly


# ══════════════════════════════════════════════════════════════════════
#  FACTION TERRITORIES
# ══════════════════════════════════════════════════════════════════════
def draw_watercolor_blob(draw: ImageDraw.ImageDraw, x: int, y: int, rx: int, ry: int,
                         color_rgba: tuple, n_blobs: int = 6):
    r, g, b, a = color_rgba
    for i in range(n_blobs):
        offset_x = random.randint(-rx // 3, rx // 3)
        offset_y = random.randint(-ry // 3, ry // 3)
        rr = int(rx * random.uniform(0.7, 1.2))
        ry_ = int(ry * random.uniform(0.7, 1.2))
        poly = wobble_polygon(x + offset_x, y + offset_y, rr, n=28, irregularity=0.45, wobble=0.15)
        scaled = []
        for xx, yy in poly:
            dx = xx - (x + offset_x)
            dy = yy - (y + offset_y)
            dy = int(dy * (ry_ / max(rr, 1)))
            scaled.append((x + offset_x + dx, y + offset_y + dy))
        alpha = max(10, a // (i + 1))
        draw.polygon(scaled, fill=(r, g, b, alpha))


def draw_handdrawn_boundary(draw: ImageDraw.ImageDraw, x: int, y: int, rx: int, ry: int, color_rgba: tuple):
    n = 48
    pts = []
    for i in range(n):
        angle = 2 * math.pi * i / n
        rr = rx + random.randint(-rx // 6, rx // 6)
        ry_ = ry + random.randint(-ry // 6, ry // 6)
        xx = x + int(rr * math.cos(angle) * (rx / max(rx, ry)))
        yy = y + int(ry_ * math.sin(angle))
        pts.append((xx, yy))
    pts.append(pts[0])
    for i in range(len(pts) - 1):
        if i % 2 == 0:
            draw.line([pts[i], pts[i + 1]], fill=color_rgba, width=2)


# ══════════════════════════════════════════════════════════════════════
#  ROUTES & SHIPS
# ══════════════════════════════════════════════════════════════════════
def draw_dashed_route(draw: ImageDraw.ImageDraw, x1: int, y1: int, x2: int, y2: int,
                      color: tuple, dash: int = 14, gap: int = 10, width: int = 2):
    total = math.hypot(x2 - x1, y2 - y1)
    if total == 0:
        return
    steps = int(total / (dash + gap))
    for s in range(steps):
        t1 = s * (dash + gap) / total
        t2 = min(1.0, (s * (dash + gap) + dash) / total)
        dx1 = int(x1 + (x2 - x1) * t1)
        dy1 = int(y1 + (y2 - y1) * t1)
        dx2 = int(x1 + (x2 - x1) * t2)
        dy2 = int(y1 + (y2 - y1) * t2)
        draw.line([(dx1, dy1), (dx2, dy2)], fill=color, width=width)


def draw_ship_icon(draw: ImageDraw.ImageDraw, x: int, y: int, angle_deg: float,
                   color: tuple, size: int = 9):
    rad = math.radians(angle_deg)
    hull = [
        (x + int(size * 0.7 * math.cos(rad)), y + int(size * 0.7 * math.sin(rad))),
        (x + int(size * 0.4 * math.cos(rad + 2.5)), y + int(size * 0.4 * math.sin(rad + 2.5))),
        (x + int(size * 0.4 * math.cos(rad - 2.5)), y + int(size * 0.4 * math.sin(rad - 2.5))),
    ]
    draw.polygon(hull, fill=color)
    mx = (hull[1][0] + hull[2][0]) // 2
    my = (hull[1][1] + hull[2][1]) // 2
    mtop_x = mx + int(size * 0.2 * math.cos(rad))
    mtop_y = my + int(size * 0.2 * math.sin(rad))
    draw.line([(mx, my), (mtop_x, mtop_y)], fill=color, width=1)
    sail = [
        (mx, my),
        (mtop_x, mtop_y),
        (mtop_x + int(size * 0.35 * math.cos(rad + 1.2)), mtop_y + int(size * 0.35 * math.sin(rad + 1.2))),
    ]
    draw.polygon(sail, fill=color)


def draw_zigzag_line(draw: ImageDraw.ImageDraw, x1: int, y1: int, x2: int, y2: int,
                     color: tuple, width: int = 2, segments: int = 8):
    pts = [(x1, y1)]
    for i in range(1, segments):
        t = i / segments
        bx = x1 + (x2 - x1) * t
        by = y1 + (y2 - y1) * t
        perp = math.atan2(y2 - y1, x2 - x1) + math.pi / 2
        offset = 8 * (1 if i % 2 == 0 else -1)
        bx += offset * math.cos(perp)
        by += offset * math.sin(perp)
        pts.append((int(bx), int(by)))
    pts.append((x2, y2))
    for i in range(len(pts) - 1):
        draw.line([pts[i], pts[i + 1]], fill=color, width=width)


# ══════════════════════════════════════════════════════════════════════
#  COMPASS ROSES
# ══════════════════════════════════════════════════════════════════════
def draw_compass_rose(draw: ImageDraw.ImageDraw, x: int, y: int, r: int = 75, ornate: bool = True):
    draw.ellipse([x - r, y - r, x + r, y + r], outline=with_alpha(PALETTE["ink"], 180), width=2)
    for angle in range(0, 360, 5):
        rad = math.radians(angle)
        inner_r = r - (6 if angle % 10 == 0 else 3)
        ix = x + int(inner_r * math.cos(rad))
        iy = y + int(inner_r * math.sin(rad))
        ox = x + int(r * math.cos(rad))
        oy = y + int(r * math.sin(rad))
        alpha = 140 if angle % 10 == 0 else 70
        draw.line([(ix, iy), (ox, oy)], fill=with_alpha(PALETTE["ink"], alpha), width=1)

    for angle, label in [(0, "N"), (90, "E"), (180, "S"), (270, "W")]:
        rad = math.radians(angle)
        tip_x = x + int((r - 10) * math.cos(rad))
        tip_y = y + int((r - 10) * math.sin(rad))
        base_l = x + int(14 * math.cos(rad + math.pi / 2))
        base_l_y = y + int(14 * math.sin(rad + math.pi / 2))
        base_r = x + int(14 * math.cos(rad - math.pi / 2))
        base_r_y = y + int(14 * math.sin(rad - math.pi / 2))
        fill = (232, 213, 183, 220) if angle in (0, 180) else with_alpha(PALETTE["ink_light"], 200)
        draw.polygon([(tip_x, tip_y), (base_l, base_l_y), (base_r, base_r_y)],
                     fill=fill, outline=with_alpha(PALETTE["ink"], 200), width=1)
        lx = x + int((r + 22) * math.cos(rad))
        ly = y + int((r + 22) * math.sin(rad))
        draw.text((lx, ly), label, fill=with_alpha(PALETTE["ink"], 220),
                  font=load_font(16, bold=True), anchor="mm")

    for angle in [45, 135, 225, 315]:
        rad = math.radians(angle)
        tip_x = x + int((r - 22) * math.cos(rad))
        tip_y = y + int((r - 22) * math.sin(rad))
        base_l = x + int(9 * math.cos(rad + math.pi / 2))
        base_l_y = y + int(9 * math.sin(rad + math.pi / 2))
        base_r = x + int(9 * math.cos(rad - math.pi / 2))
        base_r_y = y + int(9 * math.sin(rad - math.pi / 2))
        draw.polygon([(tip_x, tip_y), (base_l, base_l_y), (base_r, base_r_y)],
                     fill=with_alpha(PALETTE["ink"], 120), outline=with_alpha(PALETTE["ink"], 160), width=1)

    inner_r = r // 3
    for angle in [0, 90, 180, 270]:
        rad = math.radians(angle)
        tip_x = x + int(inner_r * math.cos(rad))
        tip_y = y + int(inner_r * math.sin(rad))
        base_l = x + int(6 * math.cos(rad + math.pi / 2))
        base_l_y = y + int(6 * math.sin(rad + math.pi / 2))
        base_r = x + int(6 * math.cos(rad - math.pi / 2))
        base_r_y = y + int(6 * math.sin(rad - math.pi / 2))
        draw.polygon([(tip_x, tip_y), (base_l, base_l_y), (base_r, base_r_y)],
                     fill=with_alpha(PALETTE["ink"], 150))

    if ornate:
        nx = x + int((r - 4) * math.cos(0))
        ny = y + int((r - 4) * math.sin(0)) - 8
        fl = 10
        draw.polygon([(nx, ny - fl), (nx + fl // 2, ny), (nx - fl // 2, ny)],
                     fill=with_alpha(PALETTE["ink"], 200))
        draw.ellipse([nx - 3, ny - 2, nx + 3, ny + 4], fill=with_alpha(PALETTE["ink"], 200))

    draw.ellipse([x - 5, y - 5, x + 5, y + 5], fill=with_alpha(PALETTE["ink"], 220))


def draw_simple_compass(draw: ImageDraw.ImageDraw, x: int, y: int, r: int = 35):
    draw.ellipse([x - r, y - r, x + r, y + r], outline=with_alpha(PALETTE["ink"], 140), width=2)
    for angle, label in [(0, "N"), (90, "E"), (180, "S"), (270, "W")]:
        rad = math.radians(angle)
        tip_x = x + int((r - 6) * math.cos(rad))
        tip_y = y + int((r - 6) * math.sin(rad))
        base_l = x + int(6 * math.cos(rad + math.pi / 2))
        base_l_y = y + int(6 * math.sin(rad + math.pi / 2))
        base_r = x + int(6 * math.cos(rad - math.pi / 2))
        base_r_y = y + int(6 * math.sin(rad - math.pi / 2))
        draw.polygon([(tip_x, tip_y), (base_l, base_l_y), (base_r, base_r_y)],
                     fill=with_alpha(PALETTE["ink"], 140))
    draw.text((x, y - r - 14), "N", fill=with_alpha(PALETTE["ink"], 180),
              font=load_font(12, bold=True), anchor="mm")


# ══════════════════════════════════════════════════════════════════════
#  CARTOUCHES & DECORATIONS
# ══════════════════════════════════════════════════════════════════════
def draw_scroll_cartouche(draw: ImageDraw.ImageDraw, x: int, y: int, w: int, h: int,
                          fill: tuple, outline: tuple, radius: int = 20):
    draw.rounded_rectangle([x, y, x + w, y + h], radius=radius, fill=fill, outline=outline, width=2)
    draw.arc([x - 20, y + h // 2 - 15, x + 20, y + h // 2 + 15], 90, 270, fill=outline, width=2)
    draw.arc([x + w - 20, y + h // 2 - 15, x + w + 20, y + h // 2 + 15], 270, 90, fill=outline, width=2)


def draw_ornate_title(draw: ImageDraw.ImageDraw, x: int, y: int):
    title_text = "THE CONVERGENCE ARCHIPELAGO"
    subtitle_text = "Year of the Convergence — Pirate Campaign"
    font_title = load_font(34, bold=True)
    font_sub = load_font(16, italic=True)
    bbox = draw.textbbox((0, 0), title_text, font=font_title)
    text_w = bbox[2] - bbox[0]
    sub_bbox = draw.textbbox((0, 0), subtitle_text, font=font_sub)
    sub_w = sub_bbox[2] - sub_bbox[0]
    total_w = max(text_w, sub_w) + 80
    total_h = 90
    cart_x = x - total_w // 2
    cart_y = y - total_h // 2
    fill = with_alpha(PALETTE["parchment_light"], 230)
    outline = with_alpha(PALETTE["ink"], 160)
    draw_scroll_cartouche(draw, cart_x, cart_y, total_w, total_h, fill, outline, radius=24)
    draw.text((x, y - 8), title_text, fill=with_alpha(PALETTE["ink"], 240),
              font=font_title, anchor="mm")
    draw.text((x, y + 22), subtitle_text, fill=with_alpha(PALETTE["ink_light"], 200),
              font=font_sub, anchor="mm")


def draw_corner_motifs(img: Image.Image) -> Image.Image:
    overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
    d = ImageDraw.Draw(overlay)
    w, h = img.size
    c = with_alpha(PALETTE["ink"], 100)

    ax, ay = 70, h - 90
    d.line([(ax, ay - 25), (ax, ay + 25)], fill=c, width=3)
    d.arc([ax - 14, ay + 18, ax + 14, ay + 38], 0, 180, fill=c, width=2)
    d.line([(ax - 12, ay - 16), (ax + 12, ay - 16)], fill=c, width=2)

    wx, wy = w - 80, 80
    d.ellipse([wx - 22, wy - 22, wx + 22, wy + 22], outline=c, width=2)
    for angle in range(0, 360, 45):
        rad = math.radians(angle)
        x1 = wx + int(10 * math.cos(rad))
        y1 = wy + int(10 * math.sin(rad))
        x2 = wx + int(20 * math.cos(rad))
        y2 = wy + int(20 * math.sin(rad))
        d.line([(x1, y1), (x2, y2)], fill=c, width=2)
    d.ellipse([wx - 5, wy - 5, wx + 5, wy + 5], fill=c)

    sx, sy = w - 70, h - 80
    d.ellipse([sx - 12, sy - 14, sx + 12, sy + 10], outline=c, width=2)
    d.ellipse([sx - 3, sy - 4, sx + 3, sy + 2], fill=c)
    d.rectangle([sx - 10, sy + 8, sx + 10, sy + 14], outline=c, width=1)
    for dx in [-4, 0, 4]:
        d.ellipse([sx + dx - 1, sy + 10, sx + dx + 1, sy + 14], fill=c)

    return Image.alpha_composite(img, overlay)


def draw_here_be_monsters(draw: ImageDraw.ImageDraw):
    x, y = 1750, 1200
    font1 = load_font(20, italic=True)
    font2 = load_font(12, italic=True)
    draw.text((x, y), "Here Be Monsters", fill=with_alpha(PALETTE["ink_faint"], 130), font=font1, anchor="mm")
    draw.text((x, y + 24), "~ Unknown Waters ~", fill=with_alpha(PALETTE["ink_faint"], 100), font=font2, anchor="mm")


# ══════════════════════════════════════════════════════════════════════
#  LOCATION ICONS & LABELS
# ══════════════════════════════════════════════════════════════════════
def draw_location_icon(draw: ImageDraw.ImageDraw, loc: dict, px: int, py: int):
    ltype = loc["type"]
    fc = FACTIONS[loc["faction"]]["hex"]
    fc_a = with_alpha(fc, 240)
    ink = with_alpha(PALETTE["ink"], 220)
    red = with_alpha(PALETTE["treasure"], 220)
    haz = with_alpha(PALETTE["hazard"], 220)

    if ltype == "capital":
        r = 15
        draw.polygon([(px, py - r), (px + r, py), (px, py + r), (px - r, py)], fill=fc_a, outline=ink, width=2)
    elif ltype == "town":
        r = 10
        draw.ellipse([px - r, py - r, px + r, py + r], fill=fc_a, outline=ink, width=2)
    elif ltype == "landmark":
        r = 12
        draw.polygon([(px, py - r), (px + r, py + r), (px - r, py + r)], fill=fc_a, outline=ink, width=2)
    elif ltype == "hazard":
        r = 11
        draw.ellipse([px - r, py - r, px + r, py + r], fill=(120, 90, 70, 220), outline=haz, width=2)
    elif ltype == "finale":
        r = 17
        draw.ellipse([px - r, py - r, px + r, py + r], fill=(30, 20, 18, 240), outline=red, width=3)
    elif ltype == "ritual":
        r = 12
        draw.ellipse([px - r, py - r, px + r, py + r], outline=ink, width=2)
        for angle in range(0, 360, 60):
            rad = math.radians(angle)
            sx = px + int(8 * math.cos(rad))
            sy = py + int(8 * math.sin(rad))
            draw.ellipse([sx - 3, sy - 3, sx + 3, sy + 3], fill=ink)
    elif ltype == "gallows":
        r = 10
        draw.ellipse([px - r, py - r, px + r, py + r], fill=(100, 50, 35, 200), outline=haz, width=2)
        draw.line([(px, py - 8), (px, py + 8)], fill=haz, width=2)
        draw.line([(px - 7, py - 4), (px + 7, py - 4)], fill=haz, width=2)
    elif ltype == "mobile":
        r = 14
        draw.ellipse([px - r, py - r, px + r, py + r], fill=(60, 140, 100, 220), outline=(30, 80, 50, 240), width=2)
        draw.line([(px, py - 10), (px, py + 10)], fill=(30, 80, 50, 200), width=1)
        draw.line([(px - 10, py), (px + 10, py)], fill=(30, 80, 50, 200), width=1)
    elif ltype == "underwater":
        r = 12
        draw.ellipse([px - r, py - r, px + r, py + r], fill=(25, 70, 110, 220), outline=(60, 140, 200, 220), width=2)
        for wy in [py - 18, py - 22, py - 26]:
            draw.arc([px - 10, wy - 3, px + 10, wy + 3], 0, 180, fill=(60, 140, 200, 180), width=1)
    elif ltype == "temple":
        r = 13
        draw.polygon([(px, py - r), (px + r, py + r), (px - r, py + r)], fill=(25, 20, 35, 230), outline=(90, 70, 110, 220), width=2)
        draw.ellipse([px - 4, py - 2, px + 4, py + 6], fill=(170, 140, 190, 220))
        draw.ellipse([px - 2, py + 1, px + 2, py + 4], fill=(25, 20, 35, 230))
    elif ltype == "spire":
        r = 15
        draw.polygon([(px, py - r - 8), (px + r, py + r), (px - r, py + r)], fill=(35, 30, 50, 240), outline=(100, 80, 120, 220), width=2)
        draw.ellipse([px - 5, py - r - 14, px + 5, py - r - 4], fill=(200, 180, 220, 200))
    elif ltype == "flying":
        r = 14
        draw.ellipse([px - r, py - r, px + r, py + r], fill=(190, 205, 220, 220), outline=(140, 160, 180, 200), width=2)
        draw.rectangle([px - 5, py - 8, px + 5, py + 3], fill=(140, 155, 175, 220))
        draw.rectangle([px - 2, py - 12, px + 2, py - 8], fill=(140, 155, 175, 220))
    elif ltype == "monster":
        r = 16
        draw.ellipse([px - r, py - r, px + r, py + r], fill=(18, 12, 30, 240), outline=(130, 40, 130, 240), width=3)
        for angle in [30, 90, 150, 210, 270, 330]:
            rad = math.radians(angle)
            tx = px + int(19 * math.cos(rad))
            ty = py + int(19 * math.sin(rad))
            draw.line([(px, py), (tx, ty)], fill=(110, 30, 110, 220), width=2)
    elif ltype == "beach":
        r = 10
        draw.ellipse([px - r, py - r, px + r, py + r], fill=(50, 42, 38, 240), outline=(160, 130, 100, 220), width=2)
        draw.ellipse([px - 4, py - 4, px + 4, py + 2], fill=(190, 170, 140, 220))
        draw.rectangle([px - 5, py + 2, px + 5, py + 7], fill=(190, 170, 140, 220))
    else:
        r = 6
        draw.ellipse([px - r, py - r, px + r, py + r], fill=fc_a, outline=ink, width=1)


def draw_banner_label(draw: ImageDraw.ImageDraw, px: int, py: int, text: str,
                      font: ImageFont.FreeTypeFont, text_color: tuple, bg_color: tuple, pad: int = 6):
    bbox = draw.textbbox((0, 0), text, font=font)
    tw = bbox[2] - bbox[0]
    th = bbox[3] - bbox[1]
    x1, y1 = px - tw // 2 - pad, py - th // 2 - pad
    x2, y2 = px + tw // 2 + pad, py + th // 2 + pad
    draw.rounded_rectangle([x1, y1, x2, y2], radius=4, fill=bg_color, outline=with_alpha(PALETTE["ink"], 120), width=1)
    tail = 6
    draw.polygon([(x1, y2), (x1 - tail, y2 + tail), (x1, y2 + tail)], fill=bg_color)
    draw.polygon([(x2, y2), (x2 + tail, y2 + tail), (x2, y2 + tail)], fill=bg_color)
    draw.text((px, py), text, fill=text_color, font=font, anchor="mm")


def draw_location_label(draw: ImageDraw.ImageDraw, loc: dict, px: int, py: int, important: bool = False):
    ltype = loc["type"]
    name = loc["name"]
    if important:
        font = load_font(18, bold=True)
        color = with_alpha(PALETTE["ink"], 240)
        offset = 26
    elif ltype in ("capital", "finale", "monster", "mobile", "flying", "spire"):
        font = load_font(16, bold=True)
        color = with_alpha(PALETTE["ink"], 230)
        offset = 24
    elif ltype in ("underwater", "hazard", "beach", "temple"):
        font = load_font(14, italic=True)
        color = with_alpha(PALETTE["ink_light"], 220)
        offset = 20
    else:
        font = load_font(13)
        color = with_alpha(PALETTE["ink"], 210)
        offset = 18

    if important:
        bg = with_alpha(PALETTE["parchment_light"], 210)
        draw_banner_label(draw, px, py + offset, name, font, color, bg)
    else:
        draw.text((px, py + offset), name, fill=color, font=font, anchor="mm")

    if ltype == "mobile":
        draw.text((px, py + offset + 16), "(moves)", fill=(40, 120, 70, 200), font=load_font(10), anchor="mm")
    if ltype == "flying":
        draw.text((px, py + offset + 16), "(drifts)", fill=(100, 130, 160, 200), font=load_font(10), anchor="mm")


# ══════════════════════════════════════════════════════════════════════
#  MARGINALIA & MARKERS
# ══════════════════════════════════════════════════════════════════════
def draw_treasure_marks(draw: ImageDraw.ImageDraw):
    c = with_alpha(PALETTE["treasure"], 210)
    for tx, ty in TREASURE_MARKS:
        px, py = cx(tx), cy(ty)
        rr = 10
        draw.line([(px - rr, py - rr), (px + rr, py + rr)], fill=c, width=2)
        draw.line([(px + rr, py - rr), (px - rr, py + rr)], fill=c, width=2)


def draw_anomaly_zones(draw: ImageDraw.ImageDraw):
    for zone in ANOMALY_ZONES:
        px, py = cx(zone["x"]), cy(zone["y"])
        rr = cs(zone["r"])
        for i, mult in enumerate([1.0, 0.7, 0.45]):
            cr = int(rr * mult)
            alpha = 50 - i * 12
            draw.ellipse([px - cr, py - cr, px + cr, py + cr], outline=(140, 180, 210, alpha), width=1)
        if zone.get("label"):
            draw.text((px, py - rr - 10), zone["label"], fill=(120, 150, 180, 160),
                      font=load_font(10, italic=True), anchor="mm")


def draw_anchorage_points(draw: ImageDraw.ImageDraw):
    anchors = [(520, 470), (700, 640), (525, 165), (720, 200)]
    c = with_alpha(PALETTE["ink_faint"], 130)
    for ax, ay in anchors:
        px, py = cx(ax), cy(ay)
        draw.line([(px, py - 8), (px, py + 8)], fill=c, width=1)
        draw.arc([px - 5, py + 4, px + 5, py + 12], 0, 180, fill=c, width=1)
        draw.line([(px - 5, py - 4), (px + 5, py - 4)], fill=c, width=1)


def draw_cartographer_note(draw: ImageDraw.ImageDraw):
    note1 = "Chart compiled by the Cartographer's Guild"
    note2 = "Year of the Convergence"
    font = load_font(12, italic=True)
    x, y = CANVAS_W - 320, CANVAS_H - 120
    draw.text((x, y), note1, fill=with_alpha(PALETTE["ink_faint"], 160), font=font, anchor="lm")
    draw.text((x, y + 18), note2, fill=with_alpha(PALETTE["ink_faint"], 160), font=font, anchor="lm")


def draw_bearing_notes(draw: ImageDraw.ImageDraw):
    notes = [("NW by N", 525, 155), ("E by S", 785, 240), ("SW", 215, 745)]
    font = load_font(10, italic=True)
    for text, ax, ay in notes:
        draw.text((cx(ax), cy(ay) - 22), text, fill=with_alpha(PALETTE["ink_faint"], 130),
                  font=font, anchor="mm")


# ══════════════════════════════════════════════════════════════════════
#  LEGEND & SCALE
# ══════════════════════════════════════════════════════════════════════
def draw_legend_cartouche(draw: ImageDraw.ImageDraw, x: int, y: int):
    n = len(FACTIONS)
    line_h = 32
    box_h = 60 + n * line_h
    box_w = 240
    fill = with_alpha(PALETTE["parchment_light"], 230)
    outline = with_alpha(PALETTE["ink"], 150)
    draw_scroll_cartouche(draw, x, y, box_w, box_h, fill, outline, radius=16)
    draw.text((x + 20, y + 20), "Factions", fill=with_alpha(PALETTE["ink"], 240),
              font=load_font(16, bold=True), anchor="lm")
    for i, (name, data) in enumerate(FACTIONS.items()):
        ly = y + 48 + i * line_h
        fc = data["hex"]
        draw.rectangle([x + 20, ly - 9, x + 42, ly + 9], fill=(fc[0], fc[1], fc[2], 230),
                       outline=with_alpha(PALETTE["ink"], 140), width=1)
        draw.text((x + 50, ly), name, fill=with_alpha(PALETTE["ink"], 220),
                  font=load_font(13), anchor="lm")


def draw_scale_bar(draw: ImageDraw.ImageDraw, x: int, y: int, bar_len: int = 280):
    c = with_alpha(PALETTE["ink"], 180)
    draw.line([(x, y), (x + bar_len, y)], fill=c, width=4)
    draw.line([(x, y - 10), (x, y + 10)], fill=c, width=3)
    draw.line([(x + bar_len, y - 10), (x + bar_len, y + 10)], fill=c, width=3)
    draw.line([(x + bar_len // 2, y - 6), (x + bar_len // 2, y + 6)], fill=c, width=2)
    draw.text((x + bar_len // 2, y + 22), "0", fill=c, font=load_font(12), anchor="mm")
    draw.text((x + bar_len, y + 22), "400 miles", fill=c, font=load_font(12), anchor="mm")


# ══════════════════════════════════════════════════════════════════════
#  MAIN
# ══════════════════════════════════════════════════════════════════════
def main():
    print("Rendering the Convergence Archipelago...")

    # ── Parchment base ──────────────────────────────────────────────
    print("  Parchment texture...")
    canvas = Image.new("RGBA", (CANVAS_W, CANVAS_H), (0, 0, 0, 0))
    parchment = make_parchment_layer(CANVAS_W, CANVAS_H, seed=SEED)
    parchment = add_stain_rings(parchment, seed=SEED, n_rings=3)
    parchment = add_fold_creases(parchment)
    canvas.paste(parchment, (0, 0))

    # ── Compute island mask ─────────────────────────────────────────
    island_mask = np.zeros((CANVAS_H, CANVAS_W), dtype=bool)
    for loc in LOCATIONS:
        if loc["island_r"] > 0:
            px, py = cx(loc["x"]), cy(loc["y"])
            r = cs(loc["island_r"])
            poly = wobble_polygon(px, py, r, n=24, irregularity=0.35, wobble=0.12)
            mask, minx, miny = polygon_bounds_mask(poly, CANVAS_W, CANVAS_H)
            island_mask[miny:miny + mask.shape[0], minx:minx + mask.shape[1]] |= mask

    # ── Ocean ────────────────────────────────────────────────────────
    print("  Ocean...")
    ocean = make_ocean_layer(CANVAS_W, CANVAS_H, island_mask, seed=SEED)
    ocean = add_wave_lines(ocean, island_mask, seed=SEED)
    ocean = add_rhumb_lines(ocean, [(CANVAS_W - 140, 140), (140, CANVAS_H - 140), (CANVAS_W // 2, CANVAS_H // 2)], island_mask)
    ocean = add_sea_monsters(ocean, island_mask)
    canvas = Image.alpha_composite(ocean, canvas)

    draw = ImageDraw.Draw(canvas)

    # ── Faction watercolor territories ─────────────────────────────
    print("  Faction territories...")
    territory_layer = Image.new("RGBA", (CANVAS_W, CANVAS_H), (0, 0, 0, 0))
    td = ImageDraw.Draw(territory_layer)
    for terr in TERRITORIES:
        fc = FACTIONS[terr["faction"]]["hex"]
        color = (fc[0], fc[1], fc[2], 45)
        draw_watercolor_blob(td, cx(terr["x"]), cy(terr["y"]), cs(terr["rx"]), cs(terr["ry"]), color)
        boundary_color = (fc[0], fc[1], fc[2], 130)
        draw_handdrawn_boundary(td, cx(terr["x"]), cy(terr["y"]), cs(terr["rx"]), cs(terr["ry"]), boundary_color)
    canvas = Image.alpha_composite(canvas, territory_layer)
    draw = ImageDraw.Draw(canvas)

    # ── Coastal glow ──────────────────────────────────────────────────
    print("  Coastal glow...")
    try:
        from scipy.ndimage import distance_transform_edt
        dist_to_land = distance_transform_edt(~island_mask)
        glow = (dist_to_land > 0) & (dist_to_land <= 30)
        pixels = canvas.load()
        for i in range(CANVAS_H):
            for j in range(CANVAS_W):
                if glow[i, j]:
                    t = dist_to_land[i, j] / 30.0
                    t = t * t
                    orig = pixels[j, i]
                    r = int(PALETTE["coast_glow"][0] * (1 - t) + orig[0] * t)
                    g = int(PALETTE["coast_glow"][1] * (1 - t) + orig[1] * t)
                    b = int(PALETTE["coast_glow"][2] * (1 - t) + orig[2] * t)
                    pixels[j, i] = (r, g, b, orig[3])
    except Exception:
        pass
    draw = ImageDraw.Draw(canvas)

    # ── Islands with detail ──────────────────────────────────────────
    print("  Islands...")
    for loc in LOCATIONS:
        if loc["island_r"] > 0:
            px, py = cx(loc["x"]), cy(loc["y"])
            r = cs(loc["island_r"])
            draw_island_with_detail(draw, px, py, r, loc["faction"], loc["type"])

    # ── Routes ──────────────────────────────────────────────────────
    print("  Routes...")
    loc_map = {loc["name"]: (cx(loc["x"]), cy(loc["y"])) for loc in LOCATIONS}
    for a_name, b_name in ROUTES:
        if a_name not in loc_map or b_name not in loc_map:
            continue
        x1, y1 = loc_map[a_name]
        x2, y2 = loc_map[b_name]
        # Glow halo
        draw_dashed_route(draw, x1, y1, x2, y2, with_alpha(PALETTE["parchment_light"], 130), dash=14, gap=10, width=5)
        # Main dashed line
        draw_dashed_route(draw, x1, y1, x2, y2, with_alpha(PALETTE["ink"], 150), dash=14, gap=10, width=2)
        # Ship icon at midpoint
        mid_x, mid_y = (x1 + x2) // 2, (y1 + y2) // 2
        angle = math.degrees(math.atan2(y2 - y1, x2 - x1))
        draw_ship_icon(draw, mid_x, mid_y, angle, with_alpha(PALETTE["ink"], 120), size=9)

    # Hazardous path through Shattered Reefs
    if "Shattered Reefs" in loc_map:
        x1, y1 = loc_map["Shattered Reefs"]
        if "Storm-Lanes" in loc_map:
            x2, y2 = loc_map["Storm-Lanes"]
            draw_zigzag_line(draw, x1, y1, x2, y2, with_alpha(PALETTE["hazard"], 180), width=2, segments=8)
        if "The Flotilla" in loc_map:
            x2, y2 = loc_map["The Flotilla"]
            draw_zigzag_line(draw, x1, y1, x2, y2, with_alpha(PALETTE["hazard"], 160), width=2, segments=7)

    # ── Anomalies, treasure, anchorage ──────────────────────────────
    print("  Markers...")
    draw_anomaly_zones(draw)
    draw_treasure_marks(draw)
    draw_anchorage_points(draw)

    # ── Region labels ──────────────────────────────────────────────
    print("  Region labels...")
    for rl in REGION_LABELS:
        px, py = cx(rl["x"]), cy(rl["y"])
        fc = FACTIONS[rl["faction"]]["hex"]
        draw.text((px, py), rl["text"], fill=(fc[0], fc[1], fc[2], 150),
                  font=load_font(16, italic=True), anchor="mm")

    for terr in TERRITORIES:
        px, py = cx(terr["x"]), cy(terr["y"])
        fc = FACTIONS[terr["faction"]]["hex"]
        draw.text((px, py - cs(terr["ry"]) - 12), terr["name"],
                  fill=(fc[0], fc[1], fc[2], 160), font=load_font(11, italic=True), anchor="mm")

    # ── Locations ───────────────────────────────────────────────────
    print("  Locations...")
    important = {"Port Veridian", "The Convergence Spire", "Kraken's Lair"}
    for loc in LOCATIONS:
        px, py = cx(loc["x"]), cy(loc["y"])
        draw_location_icon(draw, loc, px, py)
        if loc.get("label", False):
            draw_location_label(draw, loc, px, py, important=loc["name"] in important)

    # ── Marginalia and decorations ──────────────────────────────────
    print("  Decorations...")
    draw_here_be_monsters(draw)
    draw_bearing_notes(draw)
    draw_cartographer_note(draw)
    canvas = draw_corner_motifs(canvas)
    draw = ImageDraw.Draw(canvas)

    # ── Compass roses ────────────────────────────────────────────────
    print("  Compass roses...")
    draw_compass_rose(draw, CANVAS_W - 140, 140, r=80, ornate=True)
    draw_simple_compass(draw, 120, CANVAS_H - 120, r=32)

    # ── Legend and scale ────────────────────────────────────────────
    print("  Legend and scale...")
    draw_legend_cartouche(draw, 40, CANVAS_H - 280)
    draw_scale_bar(draw, CANVAS_W - 420, CANVAS_H - 70, bar_len=300)

    # ── Title ─────────────────────────────────────────────────────────
    print("  Title...")
    draw_ornate_title(draw, CANVAS_W // 2, 60)

    # ── Border ───────────────────────────────────────────────────────
    draw.rectangle([10, 10, CANVAS_W - 11, CANVAS_H - 11], outline=with_alpha(PALETTE["ink"], 150), width=4)
    draw.rectangle([18, 18, CANVAS_W - 19, CANVAS_H - 19], outline=with_alpha(PALETTE["ink"], 80), width=1)

    # ── Save ──────────────────────────────────────────────────────────
    final = canvas.convert("RGB")
    out_path = "/home/thesage/pirate-map/convergence_map.png"
    final.save(out_path, "PNG")
    print(f"\nMap saved to: {out_path}")
    print(f"Resolution: {CANVAS_W}x{CANVAS_H}")
    print(f"Locations: {len(LOCATIONS)}, Routes: {len(ROUTES)}, Treasure: {len(TREASURE_MARKS)}, Anomalies: {len(ANOMALY_ZONES)}")


if __name__ == "__main__":
    main()
