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

# ============================================
# CONFIGURATION — EDIT THESE FOR YOUR CAMPAIGN
# ============================================
WIDTH, HEIGHT = 1920, 1080  # Output resolution
SEED = 42                   # Change for different archipelago shapes
random.seed(SEED)
np.random.seed(SEED)

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

LOCATIONS = [
    {"name": "Port Veridian", "type": "capital", "faction": "Free Ports", "x_pct": 0.65, "y_pct": 0.45},
    {"name": "The Flotilla", "type": "town", "faction": "Free Ports", "x_pct": 0.70, "y_pct": 0.55},
    {"name": "The Convergence Spire", "type": "landmark", "faction": "The Harrowed", "x_pct": 0.30, "y_pct": 0.40},
    {"name": "The Sargasso Maze", "type": "hazard", "faction": "The Displaced", "x_pct": 0.25, "y_pct": 0.60},
    {"name": "The Black Reach", "type": "finale", "faction": "The Harrowed", "x_pct": 0.15, "y_pct": 0.80},
    {"name": "Shattered Reefs", "type": "region", "faction": "The Reef Lords", "x_pct": 0.80, "y_pct": 0.35},
    {"name": "Pearl Banks", "type": "region", "faction": "The Reef Lords", "x_pct": 0.75, "y_pct": 0.25},
    {"name": "Storm-Lanes", "type": "region", "faction": "The Reef Lords", "x_pct": 0.85, "y_pct": 0.50},
    {"name": "Salt Temple", "type": "town", "faction": "Free Ports", "x_pct": 0.68, "y_pct": 0.42},
    {"name": "Drowned Anchor", "type": "town", "faction": "Free Ports", "x_pct": 0.63, "y_pct": 0.48},
    {"name": "Vault of Tides", "type": "landmark", "faction": "Free Ports", "x_pct": 0.66, "y_pct": 0.43},
    {"name": "The Maze Below", "type": "hazard", "faction": "The Displaced", "x_pct": 0.32, "y_pct": 0.42},
    {"name": "Silver Outpost", "type": "town", "faction": "Silver Navy", "x_pct": 0.50, "y_pct": 0.20},
    {"name": "The Gutter", "type": "slum", "faction": "Free Ports", "x_pct": 0.62, "y_pct": 0.47},
    {"name": "Displaced Flotilla", "type": "hazard", "faction": "The Displaced", "x_pct": 0.20, "y_pct": 0.70},
]

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"),
]

# ============================================
# FAST FRACTAL NOISE (bilinear interpolation)
# ============================================

def fast_fractal_noise(h, w, seed=SEED, octaves=4):
    np.random.seed(seed)
    result = np.zeros((h, w), dtype=np.float32)
    amp = 1.0
    for o in range(octaves):
        gh = max(2, int(h / (16 * (2**o))))
        gw = max(2, int(w / (16 * (2**o))))
        grid = np.random.randn(gh, gw).astype(np.float32)
        from numpy import linspace
        x_src = linspace(0, gw - 1, w)
        y_src = linspace(0, gh - 1, h)
        xi = x_src.astype(np.int32)
        yi = y_src.astype(np.int32)
        xi1 = np.clip(xi + 1, 0, gw - 1)
        yi1 = np.clip(yi + 1, 0, gh - 1)
        xf = (x_src - xi).astype(np.float32)
        yf = (y_src - yi).astype(np.float32)
        xi = np.clip(xi, 0, gw - 1)
        yi = np.clip(yi, 0, gh - 1)
        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
    return result

# ============================================
# TERRAIN GENERATION
# ============================================

heightmap = fast_fractal_noise(HEIGHT, WIDTH, seed=SEED, octaves=4)
heightmap = (heightmap - heightmap.min()) / (heightmap.max() - heightmap.min())

land_mask = heightmap > 0.52   # ADJUST: lower = more land, higher = more ocean

# Add tiny atolls
np.random.seed(SEED)
for _ in range(40):
    cx, cy = np.random.randint(50, WIDTH-50), np.random.randint(50, HEIGHT-50)
    r = np.random.randint(8, 25)
    yy, xx = np.ogrid[-r:r+1, -r:r+1]
    circle = xx**2 + yy**2 <= r**2
    y1, y2 = max(0, cy-r), min(HEIGHT, cy+r+1)
    x1, x2 = max(0, cx-r), min(WIDTH, cx+r+1)
    h = y2-y1
    w = x2-x1
    land_mask[y1:y2, x1:x2] = np.logical_or(land_mask[y1:y2, x1:x2], circle[:h, :w])

# ============================================
# COLOR PALETTE
# ============================================
ocean_deep = np.array([10, 35, 60])
ocean_mid = np.array([20, 55, 90])
ocean_shallow = np.array([35, 85, 130])
sand = np.array([194, 178, 128])
grass_light = np.array([85, 130, 60])
grass_dark = np.array([45, 90, 35])
dark_rock = np.array([60, 55, 50])

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(-10, 10)
            color = np.clip(color + noise_val, 0, 255).astype(np.uint8)
            img_arr[i, j] = color
        else:
            if h < 0.35:
                color = ocean_deep
            elif h < 0.45:
                color = ocean_mid
            else:
                color = ocean_shallow
            img_arr[i, j] = color.astype(np.uint8)

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, 16)
        font_bold = ImageFont.truetype(fp, 20)
        font_small = ImageFont.truetype(fp, 12)
        break
if font is None:
    font = ImageFont.load_default()
    font_bold = font
    font_small = font

# ============================================
# DRAW SEA ROUTES (dashed lines)
# ============================================
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 = 8
        gap_len = 6
        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=(220, 200, 150), width=1)

# ============================================
# 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 = 10
        draw.polygon([(x, y-r), (x+r, y), (x, y+r), (x-r, y)], fill=fc, outline=(255,255,255))
        draw.text((x, y+15), loc["name"], fill=(240, 230, 210), font=font_bold, anchor="mm")
    elif loc["type"] == "town":
        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=(220, 210, 190), font=font, anchor="mm")
    elif loc["type"] == "landmark":
        r = 8
        draw.polygon([(x, y-r), (x+r, y+r), (x-r, y+r)], fill=fc, outline=(255,255,255))
        draw.text((x, y+14), loc["name"], fill=(255, 220, 180), font=font, anchor="mm")
    elif loc["type"] == "hazard":
        r = 7
        draw.ellipse([x-r, y-r, x+r, y+r], fill=(100, 80, 60), outline=(200, 50, 50))
        draw.text((x, y+12), loc["name"], fill=(200, 160, 120), font=font, anchor="mm")
    elif loc["type"] == "finale":
        r = 12
        draw.ellipse([x-r, y-r, x+r, y+r], fill=(20, 10, 10), outline=(200, 50, 50))
        draw.text((x, y+16), loc["name"], fill=(180, 50, 50), font=font_bold, anchor="mm")
    elif loc["type"] == "region":
        draw.text((x, y), loc["name"], fill=(200, 200, 200), font=font_small, anchor="mm")
    else:
        r = 5
        draw.ellipse([x-r, y-r, x+r, y+r], fill=fc, outline=(255,255,255))
        draw.text((x, y+10), loc["name"], fill=(220, 210, 190), font=font_small, anchor="mm")

# ============================================
# COMPASS ROSE
# ============================================
cx, cy = WIDTH - 80, 80
cr = 40
draw.ellipse([cx-cr, cy-cr, cx+cr, cy+cr], outline=(180, 160, 120), width=2)
draw.polygon([(cx, cy-cr+5), (cx+8, cy), (cx-8, cy)], fill=(180, 160, 120))
draw.text((cx, cy-cr-15), "N", fill=(220, 200, 150), font=font_bold, anchor="mm")
draw.text((cx, cy+cr+10), "S", fill=(180, 160, 120), font=font, anchor="mm")
draw.text((cx-cr-10, cy), "W", fill=(180, 160, 120), font=font, anchor="mm")
draw.text((cx+cr+10, cy), "E", fill=(180, 160, 120), font=font, anchor="mm")

# ============================================
# MAP TITLE
# ============================================
draw.text((WIDTH//2, 35), "The Convergence Archipelago", fill=(240, 220, 180), font=font_bold, anchor="mm")
draw.text((WIDTH//2, 60), "Year of the Convergence — Pirate Campaign", fill=(180, 160, 130), font=font, anchor="mm")

# ============================================
# FACTION LEGEND
# ============================================
legend_x, legend_y = 30, HEIGHT - 140
draw.rectangle([legend_x-10, legend_y-10, legend_x+200, legend_y+130], fill=(10, 20, 30), outline=(120, 100, 80), width=1)
draw.text((legend_x, legend_y), "Factions", fill=(220, 200, 150), font=font_bold)
for i, (name, data) in enumerate(FACTIONS.items()):
    ly = legend_y + 25 + i * 22
    fc = tuple(int(data["color"][j:j+2], 16) for j in (1, 3, 5))
    draw.rectangle([legend_x, ly-6, legend_x+12, ly+6], fill=fc, outline=(255,255,255))
    draw.text((legend_x+18, ly), name, fill=(200, 190, 170), font=font, anchor="lm")

# ============================================
# SCALE BAR
# ============================================
sx, sy = WIDTH - 200, HEIGHT - 40
bar_len = 150
draw.line([(sx, sy), (sx+bar_len, sy)], fill=(180, 160, 120), width=3)
draw.line([(sx, sy-5), (sx, sy+5)], fill=(180, 160, 120), width=2)
draw.line([(sx+bar_len, sy-5), (sx+bar_len, sy+5)], fill=(180, 160, 120), width=2)
draw.text((sx+bar_len//2, sy+15), "300 miles", fill=(180, 160, 120), font=font, anchor="mm")

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

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']}")
