#!/usr/bin/env python3
"""
Expanded Procedural Archipelago Map Generator
=============================================
Supports 23 location types, faction territory shading, patrol routes,
mobile entity paths, storm zones, dead zones, timeline fissures,
generated ports/islands, and terrain overrides.

See campaign-cartography SKILL.md for the full location type reference.
"""

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 = {
    "Faction A": {"color": "#c0392b", "territory": "east",   "patrol_color": "#e74c3c"},
    "Faction B": {"color": "#95a5a6", "territory": "north",  "patrol_color": "#bdc3c7"},
    "Faction C": {"color": "#8e44ad", "territory": "south",  "patrol_color": "#af7ac5"},
    "Faction D": {"color": "#2c3e50", "territory": "west",   "patrol_color": "#5d6d7e"},
    "Faction E": {"color": "#27ae60", "territory": "center", "patrol_color": "#2ecc71"},
}

LOCATIONS = [
    # Core types: capital, town, landmark, hazard, finale, region, slum
    # Advanced types: beach, ritual_site, mobile_base, underwater, monster_lair,
    #   flying, hidden_temple, shipwreck, storm_zone, dead_zone, gallows,
    #   false_lighthouse, smuggler_cove, diving_site, monster_territory, fissure
    {"name": "Capital City", "type": "capital", "faction": "Faction E", "x_pct": 0.50, "y_pct": 0.50},
]

ROUTES = [
    ("Capital City", "Town A"),
]

PATROL_ROUTES = [
    {"name": "Captain Name (Faction)", "faction": "Faction A",
     "waypoints": [(0.80, 0.30), (0.85, 0.25), (0.90, 0.20)]},
]

MOBILE_PATHS = [
    {"name": "Mobile Entity Route", "waypoints": [(0.50, 0.55), (0.55, 0.50)],
     "color": (100, 200, 140), "dash": (12, 6)},
]

# ============================================
# TERRAIN: Island blob functions
# ============================================

def add_blob(mask, cx, cy, rx, ry, rotation=0, noise_strength=0.3):
    y, x = np.ogrid[:HEIGHT, :WIDTH]
    cos_r, sin_r = math.cos(rotation), math.sin(rotation)
    dx = x - cx
    dy = 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):
    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

# Build land mask with add_blob/add_atoll calls for each island cluster
land_mask = np.zeros((HEIGHT, WIDTH), dtype=bool)
# ... add your island clusters here ...

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

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 via scipy.ndimage.distance_transform_edt (optional)

img = Image.fromarray(img_arr)

# ============================================
# FACTION TERRITORY SHADING
# ============================================
overlay = Image.new('RGBA', (WIDTH, HEIGHT), (0, 0, 0, 0))
overlay_draw = ImageDraw.Draw(overlay)
territory_defs = {
    "Faction A": {"center": (0.85, 0.30), "rx": 180, "ry": 280, "color": (192, 57, 43, 30)},
    # ... define per faction ...
}
for name, tdef in territory_defs.items():
    cx = int(tdef["center"][0] * WIDTH)
    cy = int(tdef["center"][1] * HEIGHT)
    overlay_draw.ellipse(
        [cx - tdef["rx"], cy - tdef["ry"], cx + tdef["rx"], cy + tdef["ry"]],
        fill=tdef["color"])
img = Image.alpha_composite(img.convert('RGBA'), overlay).convert('RGB')
draw = ImageDraw.Draw(img)

# ============================================
# STORM ZONES (RGBA overlay, then composite)
# ============================================
storm_overlay = Image.new('RGBA', (WIDTH, HEIGHT), (0, 0, 0, 0))
storm_draw = ImageDraw.Draw(storm_overlay)
for loc in LOCATIONS:
    if loc["type"] == "storm_zone":
        cx = int(loc["x_pct"] * WIDTH)
        cy = int(loc["y_pct"] * HEIGHT)
        for r in range(35, 15, -5):
            alpha = int(40 * (r / 35.0))
            storm_draw.ellipse([cx-r, cy-r, cx+r, cy+r],
                              outline=(180, 200, 220, alpha), width=2)
        for angle in range(0, 360, 30):
            rad = math.radians(angle)
            ex = cx + int(30 * math.cos(rad))
            ey = cy + int(30 * math.sin(rad))
            storm_draw.line([(cx, cy), (ex, ey)], fill=(160, 190, 220, 60), width=1)
img = Image.alpha_composite(img.convert('RGBA'), storm_overlay).convert('RGB')
draw = ImageDraw.Draw(img)

# ============================================
# FONTS
# ============================================
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 = font_bold = font_small = font_tiny = None
for fp in font_paths:
    if os.path.exists(fp):
        font = ImageFont.truetype(fp, 16)
        font_bold = ImageFont.truetype(fp, 20)
        font_small = ImageFont.truetype(fp, 12)
        font_tiny = ImageFont.truetype(fp, 10)
        break
if font is None:
    font = font_bold = font_small = font_tiny = ImageFont.load_default()

# ============================================
# DRAWING FUNCTIONS PER LOCATION TYPE
# ============================================

def draw_location(draw, loc, x, y, fc):
    """Draw a location marker based on its type. See SKILL.md for full type reference."""
    ltype = loc["type"]

    if ltype == "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 ltype == "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 ltype == "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 ltype == "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 ltype == "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 ltype == "region":
        draw.text((x, y), loc["name"], fill=(220, 220, 220), font=font_small, anchor="mm")

    elif ltype == "slum":
        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")

    elif ltype == "beach":
        r = 8
        draw.ellipse([x-r, y-r, x+r, y+r], fill=(40, 35, 30), outline=(180, 150, 120), width=2)
        draw.ellipse([x-3, y-4, x+3, y+2], fill=(200, 180, 150))
        draw.rectangle([x-4, y+2, x+4, y+6], fill=(200, 180, 150))
        draw.text((x, y+14), loc["name"], fill=(200, 180, 150), font=font_small, anchor="mm")

    elif ltype == "ritual_site":
        r = 9
        draw.ellipse([x-r, y-r, x+r, y+r], outline=(200, 180, 140), width=2)
        for angle in range(0, 360, 60):
            rad = math.radians(angle)
            sx = x + int(7 * math.cos(rad))
            sy = y + int(7 * math.sin(rad))
            draw.ellipse([sx-2, sy-2, sx+2, sy+2], fill=(180, 160, 120))
        draw.text((x, y+14), loc["name"], fill=(220, 200, 160), font=font_small, anchor="mm")

    elif ltype == "mobile_base":
        r = 12
        draw.ellipse([x-r, y-r, x+r, y+r], fill=(60, 140, 100), outline=(100, 200, 150), width=2)
        draw.ellipse([x-5, y-5, x+5, y+5], outline=(40, 100, 70), width=1)
        draw.line([(x, y-8), (x, y+8)], fill=(40, 100, 70), width=1)
        draw.line([(x-8, y), (x+8, y)], fill=(40, 100, 70), width=1)
        draw.text((x, y+18), loc["name"], fill=(100, 220, 160), font=font_bold, anchor="mm")
        draw.text((x, y+30), "(moves)", fill=(140, 200, 170), font=font_tiny, anchor="mm")

    elif ltype == "underwater":
        r = 10
        draw.ellipse([x-r, y-r, x+r, y+r], fill=(20, 60, 100), outline=(80, 160, 220), width=2)
        for wi, wy in enumerate([y-14, y-18, y-22]):
            draw.arc([x-8, wy-3, x+8, wy+3], 0, 180, fill=(80, 160, 220), width=1)
        draw.text((x, y+16), loc["name"], fill=(120, 190, 240), font=font, anchor="mm")

    elif ltype == "monster_lair":
        r = 14
        draw.ellipse([x-r, y-r, x+r, y+r], fill=(15, 10, 25), outline=(120, 40, 120), width=3)
        for angle in [30, 90, 150, 210, 270, 330]:
            rad = math.radians(angle)
            tx = x + int(16 * math.cos(rad))
            ty = y + int(16 * math.sin(rad))
            draw.line([(x, y), (tx, ty)], fill=(100, 30, 100), width=2)
        draw.text((x, y+20), loc["name"], fill=(180, 80, 180), font=font_bold, anchor="mm")

    elif ltype == "flying":
        r = 12
        draw.ellipse([x-r, y-r, x+r, y+r], fill=(180, 200, 220), outline=(220, 230, 240), width=2)
        draw.rectangle([x-4, y-6, x+4, y+2], fill=(140, 160, 180))
        draw.rectangle([x-1, y-10, x+1, y-6], fill=(140, 160, 180))
        draw.text((x, y+18), loc["name"], fill=(200, 220, 240), font=font, anchor="mm")
        draw.text((x, y+30), "(drifts)", fill=(170, 190, 210), font=font_tiny, anchor="mm")

    elif ltype == "hidden_temple":
        r = 10
        draw.polygon([(x, y-r), (x+r, y+r), (x-r, y+r)], fill=(20, 20, 30), outline=(100, 80, 120), width=2)
        draw.ellipse([x-3, y-2, x+3, y+4], fill=(180, 150, 200))
        draw.ellipse([x-1, y, x+1, y+2], fill=(20, 20, 30))
        draw.text((x, y+16), loc["name"], fill=(160, 130, 180), font=font_small, anchor="mm")

    elif ltype == "shipwreck":
        r = 7
        draw.ellipse([x-r, y-r, x+r, y+r], fill=(60, 50, 40), outline=(160, 130, 100), width=1)
        draw.polygon([(x-5, y-2), (x+5, y-2), (x+3, y+4), (x-3, y+4)],
                     fill=(80, 65, 50), outline=(140, 110, 80))
        draw.text((x, y+12), loc["name"], fill=(180, 150, 120), font=font_tiny, anchor="mm")

    elif ltype == "storm_zone":
        draw.text((x, y), loc["name"], fill=(180, 200, 220), font=font_small, anchor="mm")

    elif ltype == "dead_zone":
        r = 14
        draw.ellipse([x-r, y-r, x+r, y+r], outline=(180, 50, 50), width=2)
        draw.line([(x-8, y-8), (x+8, y+8)], fill=(180, 50, 50), width=2)
        draw.line([(x+8, y-8), (x-8, y+8)], fill=(180, 50, 50), width=2)
        draw.text((x, y+18), loc["name"], fill=(180, 60, 60), font=font_tiny, anchor="mm")

    elif ltype == "gallows":
        r = 7
        draw.ellipse([x-r, y-r, x+r, y+r], fill=(80, 40, 30), outline=(200, 100, 80), width=2)
        draw.line([(x, y-6), (x, y+6)], fill=(180, 100, 80), width=2)
        draw.line([(x-5, y-4), (x+5, y-4)], fill=(180, 100, 80), width=2)
        draw.line([(x-3, y-4), (x-3, y-1)], fill=(180, 100, 80), width=1)
        draw.text((x, y+14), loc["name"], fill=(200, 120, 100), font=font_small, anchor="mm")

    elif ltype == "false_lighthouse":
        r = 8
        draw.ellipse([x-r, y-r, x+r, y+r], fill=(60, 55, 45), outline=(200, 60, 60), width=2)
        draw.rectangle([x-3, y-5, x+3, y+5], fill=(180, 160, 120))
        draw.ellipse([x-2, y-7, x+2, y-3], fill=(255, 200, 100))
        draw.line([(x-5, y-5), (x+5, y+5)], fill=(220, 50, 50), width=2)
        draw.line([(x+5, y-5), (x-5, y+5)], fill=(220, 50, 50), width=2)
        draw.text((x, y+14), loc["name"], fill=(200, 100, 80), font=font_tiny, anchor="mm")

    elif ltype == "smuggler_cove":
        r = 5
        draw.ellipse([x-r, y-r, x+r, y+r], fill=(40, 50, 40), outline=(100, 140, 100), width=1)
        draw.line([(x, y-4), (x, y+4)], fill=(100, 140, 100), width=1)
        draw.arc([x-3, y+1, x+3, y+7], 0, 180, fill=(100, 140, 100), width=1)
        draw.line([(x-3, y-2), (x+3, y-2)], fill=(100, 140, 100), width=1)
        # No label — DM secret

    elif ltype == "diving_site":
        r = 6
        draw.ellipse([x-r, y-r, x+r, y+r], fill=(30, 70, 100), outline=(80, 150, 200), width=1)
        draw.ellipse([x-2, y-3, x+2, y+1], fill=(150, 210, 240))
        draw.text((x, y+12), loc["name"], fill=(120, 180, 220), font=font_tiny, anchor="mm")

    elif ltype == "monster_territory":
        r = 8
        draw.ellipse([x-r, y-r, x+r, y+r], fill=(30, 25, 40), outline=(140, 100, 60), width=1)
        draw.arc([x-5, y-4, x+5, y+4], 0, 180, fill=(140, 100, 60), width=1)
        draw.arc([x-5, y, x+5, y+8], 180, 0, fill=(140, 100, 60), width=1)
        draw.text((x, y+14), loc["name"], fill=(160, 120, 80), font=font_tiny, anchor="mm")

    elif ltype == "fissure":
        draw.ellipse([x-4, y-4, x+4, y+4], fill=(180, 220, 255))
        for angle in range(0, 360, 45):
            rad = math.radians(angle)
            ex = x + int(10 * math.cos(rad))
            ey = y + int(10 * math.sin(rad))
            draw.line([(x, y), (ex, ey)], fill=(140, 180, 220), width=1)
        draw.text((x, y+12), loc["name"], fill=(150, 190, 230), font=font_tiny, 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")

# ============================================
# DRAW ALL LOCATIONS
# ============================================
loc_dict = {}
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
    loc_dict[loc["name"]] = (x, y)
    draw_location(draw, loc, x, y, fc)

# ============================================
# DRAW ROUTES, PATROL ROUTES, MOBILE PATHS
# ============================================
# (See the full expanded generator for dashed-line drawing logic)

# ============================================
# COMPASS ROSE, TITLE, LEGEND, SCALE BAR, BORDER
# ============================================
# (Standard elements — see template for details)

# ============================================
# 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, "map.png")
img.save(img_path, "PNG")
print(f"Map saved to: {img_path}")
