#!/usr/bin/env python3
"""
Pirate Campaign Map Generator — The Convergence Archipelago

Generates a clean, uncluttered archipelago map with:
- Hand-placed island blob clusters (not noisy Perlin specks)
- Dark navy ocean with subtle coastal glow
- Named locations with faction-colored symbols
- Dashed sea routes
- PNG + JSON export

Usage:
    python3 campaign-cartography-pirate-map-generator.py

Then open: /home/thesage/pirate-map/convergence-archipelago-map.png
"""

import numpy as np
import matplotlib
matplotlib.use('Agg')
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import json, os, math, random

# ============================================
# CONFIGURATION
# ============================================
WIDTH, HEIGHT = 1920, 1080
SEED = 42
random.seed(SEED)
np.random.seed(SEED)

FACTIONS = {
    "The Reef Lords": {"color": "#c0392b", "territory": "east"},
    "Silver Navy": {"color": "#95a5a6", "territory": "north"},
    "The Displaced": {"color": "#8e44ad", "territory": "south"},
    "The Harrowed": {"color": "#2c3e50", "territory": "west"},
    "Free Ports": {"color": "#27ae60", "territory": "center"}
}

# Define your campaign locations as percentage coordinates (0.0–1.0)
LOCATIONS = [
    {"name": "Port Veridian", "type": "capital", "faction": "Free Ports", "x_pct": 0.72, "y_pct": 0.48},
    {"name": "The Flotilla", "type": "town", "faction": "Free Ports", "x_pct": 0.82, "y_pct": 0.62},
    {"name": "The Convergence Spire", "type": "landmark", "faction": "The Harrowed", "x_pct": 0.25, "y_pct": 0.35},
    {"name": "The Sargasso Maze", "type": "hazard", "faction": "The Displaced", "x_pct": 0.18, "y_pct": 0.68},
    {"name": "The Black Reach", "type": "finale", "faction": "The Harrowed", "x_pct": 0.10, "y_pct": 0.88},
    {"name": "Shattered Reefs", "type": "region", "faction": "The Reef Lords", "x_pct": 0.88, "y_pct": 0.28},
    {"name": "Pearl Banks", "type": "region", "faction": "The Reef Lords", "x_pct": 0.85, "y_pct": 0.15},
    {"name": "Storm-Lanes", "type": "region", "faction": "The Reef Lords", "x_pct": 0.92, "y_pct": 0.52},
    {"name": "Salt Temple", "type": "town", "faction": "Free Ports", "x_pct": 0.68, "y_pct": 0.38},
    {"name": "Drowned Anchor", "type": "town", "faction": "Free Ports", "x_pct": 0.62, "y_pct": 0.55},
    {"name": "Vault of Tides", "type": "landmark", "faction": "Free Ports", "x_pct": 0.70, "y_pct": 0.42},
    {"name": "The Maze Below", "type": "hazard", "faction": "The Displaced", "x_pct": 0.30, "y_pct": 0.42},
    {"name": "Silver Outpost", "type": "town", "faction": "Silver Navy", "x_pct": 0.55, "y_pct": 0.18},
    {"name": "The Gutter", "type": "slum", "faction": "Free Ports", "x_pct": 0.60, "y_pct": 0.52},
    {"name": "Displaced Flotilla", "type": "hazard", "faction": "The Displaced", "x_pct": 0.15, "y_pct": 0.78},
]

ROUTES = [
    ("Port Veridian", "The Flotilla"),
    ("Port Veridian", "Salt Temple"),
    ("Port Veridian", "Drowned Anchor"),
    ("The Flotilla", "Shattered Reefs"),
    ("Port Veridian", "Silver Outpost"),
    ("The Convergence Spire", "The Maze Below"),
    ("Port Veridian", "The Convergence Spire"),
    ("The Sargasso Maze", "Displaced Flotilla"),
]

# ============================================
# TERRAIN: Large deliberate island clusters
# ============================================

def add_blob(mask, cx, cy, rx, ry, rotation=0, noise_strength=0.3):
    """Add an irregular island blob centered at (cx, cy)."""
    y, x = np.ogrid[:HEIGHT, :WIDTH]
    cos_r, sin_r = math.cos(rotation), math.sin(rotation)
    dx, dy = x - cx, y - cy
    rdx = dx * cos_r + dy * sin_r
    rdy = -dx * sin_r + dy * cos_r
    dist = (rdx / max(rx, 1))**2 + (rdy / max(ry, 1))**2
    noise = np.random.randn(HEIGHT, WIDTH) * noise_strength
    blob = (dist + noise * 0.3) < 1.0
    mask |= blob
    return mask

def add_atoll(mask, cx, cy, r_outer, r_inner=None):
    """Add a ring-shaped atoll."""
    if r_inner is None:
        r_inner = max(3, r_outer // 2)
    y, x = np.ogrid[:HEIGHT, :WIDTH]
    dist = np.sqrt((x - cx)**2 + (y - cy)**2)
    noise = np.random.randn(HEIGHT, WIDTH) * 1.5
    ring = (dist + noise < r_outer) & (dist - noise > r_inner)
    mask |= ring
    return mask

# Start with empty ocean
land_mask = np.zeros((HEIGHT, WIDTH), dtype=bool)

# === CLUSTER 1: Eastern Reef Lords Territory ===
add_blob(land_mask, 1680, 300, 140, 90, rotation=0.3)
add_blob(land_mask, 1630, 160, 80, 50, rotation=-0.2)
add_blob(land_mask, 1760, 560, 60, 100, rotation=0.5)
add_atoll(land_mask, 1720, 220, 25, 12)
add_atoll(land_mask, 1800, 400, 20, 8)

# === CLUSTER 2: Central Free Ports Hub ===
add_blob(land_mask, 1380, 520, 110, 80, rotation=0.1)
add_blob(land_mask, 1570, 670, 70, 50, rotation=0.4)
add_blob(land_mask, 1300, 410, 40, 30, rotation=-0.1)
add_blob(land_mask, 1350, 460, 25, 20)
add_blob(land_mask, 1190, 600, 35, 25, rotation=0.2)
add_blob(land_mask, 1150, 570, 20, 15)
add_atoll(land_mask, 1450, 580, 30, 15)
add_atoll(land_mask, 1520, 480, 22, 10)

# === CLUSTER 3: Western Harrowed Territory ===
add_blob(land_mask, 480, 380, 90, 70, rotation=-0.3)
add_blob(land_mask, 580, 460, 40, 30, rotation=0.2)
add_atoll(land_mask, 420, 320, 18, 8)
add_atoll(land_mask, 550, 520, 15, 6)

# === CLUSTER 4: Southern Displaced Territory ===
add_blob(land_mask, 350, 740, 100, 70, rotation=0.4)
add_blob(land_mask, 290, 850, 50, 40, rotation=-0.2)
add_atoll(land_mask, 300, 780, 20, 10)
add_atoll(land_mask, 400, 820, 15, 7)

# === CLUSTER 5: Northern Silver Navy Outpost ===
add_blob(land_mask, 1060, 200, 60, 45, rotation=0.1)
add_atoll(land_mask, 1120, 240, 18, 8)

# === The Black Reach (far SW, cursed) ===
add_blob(land_mask, 200, 960, 50, 35, rotation=0.6)
add_blob(land_mask, 250, 920, 30, 20, rotation=-0.3)
add_blob(land_mask, 150, 980, 25, 15)

# === Scatter a few distant atolls for flavor ===
np.random.seed(SEED)
for _ in range(12):
    cx = np.random.randint(150, WIDTH-150)
    cy = np.random.randint(150, HEIGHT-150)
    r = np.random.randint(6, 14)
    land_mask = add_atoll(land_mask, cx, cy, r, max(2, r//2))

# ============================================
# HEIGHTMAP (simple random for terrain variation)
# ============================================
heightmap = np.random.rand(HEIGHT, WIDTH).astype(np.float32) * 0.3 + 0.35
heightmap[land_mask] += 0.4
heightmap = np.clip(heightmap, 0, 1)

# ============================================
# COLOR PALETTE — clean tropical look
# ============================================
ocean_deep = np.array([6, 22, 45])
ocean_mid = np.array([14, 48, 88])
ocean_shallow = np.array([25, 82, 135])
sand = np.array([215, 200, 160])
grass_light = np.array([100, 165, 75])
grass_dark = np.array([55, 110, 50])
dark_rock = np.array([75, 70, 65])

img_arr = np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8)

for i in range(HEIGHT):
    for j in range(WIDTH):
        h = heightmap[i, j]
        if land_mask[i, j]:
            if h < 0.55:
                color = sand
            elif h < 0.65:
                color = grass_light
            elif h < 0.78:
                color = grass_dark
            else:
                color = dark_rock
            noise_val = np.random.randint(-5, 5)
            color = np.clip(color + noise_val, 0, 255).astype(np.uint8)
            img_arr[i, j] = color
        else:
            color = ocean_deep if h < 0.42 else ocean_mid
            img_arr[i, j] = color.astype(np.uint8)

# ============================================
# COASTAL GLOW — lighter shallows around islands
# ============================================
try:
    from scipy.ndimage import distance_transform_edt
    dist_to_land = distance_transform_edt(~land_mask)
    glow_mask = (dist_to_land > 0) & (dist_to_land <= 18)
    for i in range(HEIGHT):
        for j in range(WIDTH):
            if glow_mask[i, j]:
                t = dist_to_land[i, j] / 18.0
                r = int(ocean_shallow[0] * (1-t) + ocean_mid[0] * t)
                g = int(ocean_shallow[1] * (1-t) + ocean_mid[1] * t)
                b = int(ocean_shallow[2] * (1-t) + ocean_mid[2] * t)
                img_arr[i, j] = (r, g, b)
except ImportError:
    pass

img = Image.fromarray(img_arr)
draw = ImageDraw.Draw(img)

# Font loading
font_paths = [
    "/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf",
    "/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf",
    "/usr/share/fonts/truetype/freefont/FreeSerif.ttf",
    "/usr/share/fonts/truetype/noto/NotoSerif-Regular.ttf",
]
font = None
font_bold = None
font_small = None
for fp in font_paths:
    if os.path.exists(fp):
        font = ImageFont.truetype(fp, 18)
        font_bold = ImageFont.truetype(fp, 22)
        font_small = ImageFont.truetype(fp, 14)
        break
if font is None:
    font = ImageFont.load_default()
    font_bold = font
    font_small = font

# ============================================
# DRAW SEA ROUTES
# ============================================
loc_dict = {loc["name"]: (int(loc["x_pct"] * WIDTH), int(loc["y_pct"] * HEIGHT)) for loc in LOCATIONS}

for a_name, b_name in ROUTES:
    if a_name in loc_dict and b_name in loc_dict:
        x1, y1 = loc_dict[a_name]
        x2, y2 = loc_dict[b_name]
        total_dist = math.hypot(x2-x1, y2-y1)
        if total_dist == 0:
            continue
        dash_len, gap_len = 10, 8
        steps = int(total_dist / (dash_len + gap_len))
        for s in range(steps):
            t1 = s * (dash_len + gap_len) / total_dist
            t2 = min(1.0, (s * (dash_len + gap_len) + dash_len) / total_dist)
            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=(230, 210, 160), width=2)

# ============================================
# DRAW LOCATIONS
# ============================================
for loc in LOCATIONS:
    x = int(loc["x_pct"] * WIDTH)
    y = int(loc["y_pct"] * HEIGHT)
    fcolor = FACTIONS.get(loc["faction"], {}).get("color", "#ffffff")
    fc = tuple(int(fcolor[i:i+2], 16) for i in (1, 3, 5))
    loc["pixel_x"] = x
    loc["pixel_y"] = y
    
    if loc["type"] == "capital":
        r = 12
        draw.polygon([(x, y-r), (x+r, y), (x, y+r), (x-r, y)], fill=fc, outline=(255,255,255), width=2)
        draw.text((x, y+18), loc["name"], fill=(255, 245, 220), font=font_bold, anchor="mm")
    elif loc["type"] == "town":
        r = 7
        draw.ellipse([x-r, y-r, x+r, y+r], fill=fc, outline=(255,255,255), width=2)
        draw.text((x, y+14), loc["name"], fill=(235, 225, 205), font=font, anchor="mm")
    elif loc["type"] == "landmark":
        r = 10
        draw.polygon([(x, y-r), (x+r, y+r), (x-r, y+r)], fill=fc, outline=(255,255,255), width=2)
        draw.text((x, y+16), loc["name"], fill=(255, 230, 190), font=font, anchor="mm")
    elif loc["type"] == "hazard":
        r = 8
        draw.ellipse([x-r, y-r, x+r, y+r], fill=(120, 90, 70), outline=(220, 60, 60), width=2)
        draw.text((x, y+14), loc["name"], fill=(210, 170, 130), font=font, anchor="mm")
    elif loc["type"] == "finale":
        r = 14
        draw.ellipse([x-r, y-r, x+r, y+r], fill=(25, 15, 15), outline=(220, 60, 60), width=3)
        draw.text((x, y+20), loc["name"], fill=(200, 60, 60), font=font_bold, anchor="mm")
    elif loc["type"] == "region":
        draw.text((x, y), loc["name"], fill=(220, 220, 220), font=font_small, anchor="mm")
    else:
        r = 6
        draw.ellipse([x-r, y-r, x+r, y+r], fill=fc, outline=(255,255,255))
        draw.text((x, y+12), loc["name"], fill=(235, 225, 205), font=font_small, anchor="mm")

# ============================================
# COMPASS ROSE
# ============================================
cx, cy = WIDTH - 100, 100
cr = 50
draw.ellipse([cx-cr, cy-cr, cx+cr, cy+cr], outline=(200, 180, 140), width=3)
draw.polygon([(cx, cy-cr+8), (cx+10, cy), (cx-10, cy)], fill=(200, 180, 140))
draw.text((cx, cy-cr-18), "N", fill=(240, 220, 180), font=font_bold, anchor="mm")
draw.text((cx, cy+cr+14), "S", fill=(180, 160, 120), font=font, anchor="mm")
draw.text((cx-cr-12, cy), "W", fill=(180, 160, 120), font=font, anchor="mm")
draw.text((cx+cr+12, cy), "E", fill=(180, 160, 120), font=font, anchor="mm")
draw.ellipse([cx-15, cy-15, cx+15, cy+15], outline=(180, 160, 120), width=1)

# ============================================
# MAP TITLE
# ============================================
draw.text((WIDTH//2, 40), "The Convergence Archipelago", fill=(255, 235, 200), font=font_bold, anchor="mm")
draw.text((WIDTH//2, 70), "Year of the Convergence — Pirate Campaign", fill=(190, 170, 140), font=font, anchor="mm")

# ============================================
# FACTION LEGEND
# ============================================
legend_x, legend_y = 40, HEIGHT - 180
draw.rectangle([legend_x-15, legend_y-15, legend_x+230, legend_y+160],
               fill=(8, 15, 25), outline=(140, 120, 100), width=2)
draw.text((legend_x, legend_y), "Factions", fill=(240, 220, 180), font=font_bold)
for i, (name, data) in enumerate(FACTIONS.items()):
    ly = legend_y + 30 + i * 26
    fc = tuple(int(data["color"][j:j+2], 16) for j in (1, 3, 5))
    draw.rectangle([legend_x, ly-8, legend_x+16, ly+8], fill=fc, outline=(255,255,255), width=1)
    draw.text((legend_x+24, ly), name, fill=(215, 200, 180), font=font, anchor="lm")

# ============================================
# SCALE BAR
# ============================================
sx, sy = WIDTH - 250, HEIGHT - 50
bar_len = 200
draw.line([(sx, sy), (sx+bar_len, sy)], fill=(200, 180, 140), width=4)
draw.line([(sx, sy-8), (sx, sy+8)], fill=(200, 180, 140), width=3)
draw.line([(sx+bar_len, sy-8), (sx+bar_len, sy+8)], fill=(200, 180, 140), width=3)
draw.line([(sx+bar_len//2, sy-5), (sx+bar_len//2, sy+5)], fill=(200, 180, 140), width=2)
draw.text((sx+bar_len//2, sy+18), "0", fill=(180, 160, 120), font=font, anchor="mm")
draw.text((sx+bar_len, sy+18), "400 miles", fill=(180, 160, 120), font=font, anchor="mm")

# ============================================
# DECORATIVE BORDER
# ============================================
draw.rectangle([0, 0, WIDTH-1, HEIGHT-1], outline=(160, 140, 110), width=4)

# ============================================
# VIGNETTE + SAVE
# ============================================
img = img.filter(ImageFilter.GaussianBlur(radius=0.3))

out_dir = "/home/thesage/pirate-map"
os.makedirs(out_dir, exist_ok=True)
img_path = os.path.join(out_dir, "convergence-archipelago-map.png")
img.save(img_path, "PNG")

json_path = os.path.join(out_dir, "campaign-locations.json")
with open(json_path, "w") as f:
    json.dump({
        "map_name": "The Convergence Archipelago",
        "dimensions": {"width": WIDTH, "height": HEIGHT},
        "seed": SEED,
        "factions": FACTIONS,
        "locations": LOCATIONS,
        "routes": ROUTES,
    }, f, indent=2)

print(f"Map saved to: {img_path}")
print(f"Data saved to: {json_path}")
print(f"\nLocations placed: {len(LOCATIONS)}")
for loc in LOCATIONS:
    print(f"  - {loc['name']} ({loc['type']}) at ({loc['pixel_x']}, {loc['pixel_y']}) — {loc['faction']}")
