from __future__ import annotations

import ast
import math
import random
import re
from pathlib import Path

from PIL import Image, ImageDraw, ImageFilter


ROOT = Path(__file__).resolve().parents[1]
SCENARIO_PATH = ROOT / "src" / "game" / "data" / "scenario.ts"
OUTPUT_PATH = ROOT / "src" / "assets" / "images" / "battle" / "first-battle-map.png"
BASE_TILE = 128
TILE = 224
SEED = 314159


def s(value: float) -> int:
    return max(1, round(value * TILE / BASE_TILE))


def load_first_battle_map() -> tuple[int, int, list[list[str]]]:
    source = SCENARIO_PATH.read_text(encoding="utf-8")
    width_match = re.search(r"firstBattleMap:\s*BattleMap\s*=\s*\{\s*width:\s*(\d+)", source)
    height_match = re.search(r"firstBattleMap:[\s\S]*?height:\s*(\d+)", source)
    terrain_match = re.search(r"firstBattleMap:[\s\S]*?terrain:\s*(\[[\s\S]*?\n\s*\])\s*\n\s*\};", source)
    if not width_match or not height_match or not terrain_match:
        raise RuntimeError("Could not parse firstBattleMap from scenario.ts")

    width = int(width_match.group(1))
    height = int(height_match.group(1))
    terrain = ast.literal_eval(terrain_match.group(1))
    if len(terrain) != height or any(len(row) != width for row in terrain):
        raise RuntimeError("Parsed terrain dimensions do not match firstBattleMap width/height")
    return width, height, terrain


def clamp(value: int) -> int:
    return max(0, min(255, value))


def jitter(color: tuple[int, int, int], amount: int, rng: random.Random) -> tuple[int, int, int]:
    return tuple(clamp(channel + rng.randint(-amount, amount)) for channel in color)


def tile_box(x: int, y: int, inset: int = 0) -> tuple[int, int, int, int]:
    return (x * TILE + inset, y * TILE + inset, (x + 1) * TILE - inset, (y + 1) * TILE - inset)


def tile_center(x: int, y: int) -> tuple[int, int]:
    return (x * TILE + TILE // 2, y * TILE + TILE // 2)


def neighbor_is(terrain: list[list[str]], x: int, y: int, kinds: set[str]) -> bool:
    return 0 <= y < len(terrain) and 0 <= x < len(terrain[0]) and terrain[y][x] in kinds


def draw_base(width: int, height: int, terrain: list[list[str]], rng: random.Random) -> Image.Image:
    image = Image.new("RGB", (width * TILE, height * TILE), (94, 109, 62))
    draw = ImageDraw.Draw(image)
    palettes = {
        "plain": [(92, 112, 61), (117, 119, 68), (112, 96, 54), (78, 105, 58)],
        "road": [(157, 128, 82), (177, 147, 92), (132, 107, 72)],
        "forest": [(42, 80, 43), (56, 95, 43), (72, 102, 50), (33, 67, 40)],
        "hill": [(105, 100, 70), (128, 116, 79), (83, 91, 62)],
        "village": [(118, 98, 62), (100, 113, 64), (151, 126, 79)],
        "fort": [(94, 79, 63), (118, 98, 74), (84, 76, 63)],
        "camp": [(105, 95, 55), (123, 112, 66), (85, 109, 70)],
        "river": [(50, 93, 101), (41, 80, 92), (74, 118, 121)],
        "cliff": [(57, 54, 47), (76, 70, 56), (43, 43, 38)],
    }

    ground_noise = Image.new("RGBA", image.size, (0, 0, 0, 0))
    noise_draw = ImageDraw.Draw(ground_noise)
    for _ in range(width * height * 42):
        px = rng.randrange(width * TILE)
        py = rng.randrange(height * TILE)
        radius = rng.randint(s(2), s(11))
        color = jitter(rng.choice(palettes["plain"]), 16, rng)
        noise_draw.ellipse((px - radius, py - radius, px + radius, py + radius), fill=(*color, rng.randint(18, 42)))
    ground_noise = ground_noise.filter(ImageFilter.GaussianBlur(s(1.2)))
    image = Image.alpha_composite(image.convert("RGBA"), ground_noise).convert("RGB")
    draw = ImageDraw.Draw(image)

    tint = Image.new("RGBA", image.size, (0, 0, 0, 0))
    tint_draw = ImageDraw.Draw(tint)
    for y, row in enumerate(terrain):
        for x, kind in enumerate(row):
            base = rng.choice(palettes[kind])
            alpha = {
                "plain": 18,
                "road": 34,
                "forest": 68,
                "hill": 54,
                "village": 40,
                "fort": 52,
                "camp": 40,
                "river": 88,
                "cliff": 82,
            }[kind]
            tint_draw.rectangle(tile_box(x, y), fill=(*jitter(base, 8, rng), alpha))
            for _ in range(64):
                px = x * TILE + rng.randrange(TILE)
                py = y * TILE + rng.randrange(TILE)
                radius = rng.randint(s(1), s(3))
                color = jitter(rng.choice(palettes[kind]), 14, rng)
                draw.ellipse((px - radius, py - radius, px + radius, py + radius), fill=color)

    tint = tint.filter(ImageFilter.GaussianBlur(s(18)))
    image = Image.alpha_composite(image.convert("RGBA"), tint).convert("RGB")
    return image


def draw_grass_detail(draw: ImageDraw.ImageDraw, terrain: list[list[str]], rng: random.Random) -> None:
    for y, row in enumerate(terrain):
        for x, kind in enumerate(row):
            if kind not in {"plain", "camp", "village"}:
                continue
            for _ in range(52):
                px = x * TILE + rng.randint(s(4), TILE - s(4))
                py = y * TILE + rng.randint(s(4), TILE - s(4))
                length = rng.randint(s(5), s(15))
                color = rng.choice([(70, 116, 55), (126, 116, 54), (62, 94, 47), (148, 130, 68)])
                draw.line((px, py, px + rng.randint(-s(3), s(3)), py - length), fill=color, width=s(1))
            for _ in range(14):
                px = x * TILE + rng.randint(s(8), TILE - s(8))
                py = y * TILE + rng.randint(s(8), TILE - s(8))
                color = jitter(rng.choice([(121, 105, 58), (83, 102, 57), (151, 128, 71)]), 9, rng)
                draw.ellipse((px - s(3), py - s(2), px + s(4), py + s(2)), fill=color)


def draw_roads(draw: ImageDraw.ImageDraw, terrain: list[list[str]], rng: random.Random) -> None:
    road_kinds = {"road", "village", "fort", "camp"}
    shadow = (96, 78, 54)
    road = (181, 145, 92)
    highlight = (210, 178, 119)

    for y, row in enumerate(terrain):
        for x, kind in enumerate(row):
            if kind not in road_kinds:
                continue
            cx, cy = tile_center(x, y)
            connections = []
            for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)):
                if neighbor_is(terrain, x + dx, y + dy, road_kinds):
                    connections.append((dx, dy))
            if not connections:
                connections = [(0, 0)]

            for dx, dy in connections:
                end_x = cx + dx * TILE // 2
                end_y = cy + dy * TILE // 2
                wobble = rng.randint(-s(8), s(8))
                if dx:
                    points = [(cx, cy + wobble), ((cx + end_x) // 2, cy - wobble), (end_x, end_y + wobble)]
                elif dy:
                    points = [(cx + wobble, cy), (cx - wobble, (cy + end_y) // 2), (end_x + wobble, end_y)]
                else:
                    points = [(cx - s(34), cy), (cx, cy + s(16)), (cx + s(34), cy - s(10))]
                draw.line(points, fill=shadow, width=s(50), joint="curve")
                draw.line(points, fill=road, width=s(41), joint="curve")
                draw.line(points, fill=highlight, width=s(7), joint="curve")

            for _ in range(52):
                px = x * TILE + rng.randint(s(10), TILE - s(10))
                py = y * TILE + rng.randint(s(10), TILE - s(10))
                draw.ellipse((px - s(2), py - s(1), px + s(3), py + s(2)), fill=jitter((122, 96, 62), 10, rng))
            for _ in range(7):
                px = x * TILE + rng.randint(s(18), TILE - s(18))
                py = y * TILE + rng.randint(s(18), TILE - s(18))
                draw.line(
                    (px - s(14), py + rng.randint(-s(4), s(4)), px + s(16), py + rng.randint(-s(4), s(4))),
                    fill=jitter((102, 79, 52), 8, rng),
                    width=s(2)
                )


def draw_rivers(draw: ImageDraw.ImageDraw, terrain: list[list[str]], rng: random.Random) -> None:
    for y, row in enumerate(terrain):
        for x, kind in enumerate(row):
            if kind != "river":
                continue
            left, top, right, bottom = tile_box(x, y)
            cx, cy = tile_center(x, y)
            north = neighbor_is(terrain, x, y - 1, {"river"})
            south = neighbor_is(terrain, x, y + 1, {"river"})
            east = neighbor_is(terrain, x + 1, y, {"river"})
            west = neighbor_is(terrain, x - 1, y, {"river"})

            water = [(left + s(10), top + s(10)), (right - s(8), top + s(2)), (right - s(12), bottom - s(8)), (left + s(4), bottom - s(2))]
            if north:
                water[0] = (left + s(8), top - s(8))
                water[1] = (right - s(4), top - s(8))
            if south:
                water[2] = (right - s(10), bottom + s(8))
                water[3] = (left + s(8), bottom + s(8))
            draw.polygon(water, fill=(43, 94, 107))
            draw.polygon([(left + s(3), top + s(2)), (right - s(3), top), (right - s(8), top + s(18)), (left + s(8), top + s(20))], fill=(69, 117, 119))
            for _ in range(24):
                sx = rng.randint(left + s(12), right - s(12))
                sy = rng.randint(top + s(15), bottom - s(15))
                draw.arc((sx - s(18), sy - s(5), sx + s(18), sy + s(9)), 190, 340, fill=(135, 177, 175), width=s(1))
            for _ in range(8):
                sx = rng.randint(left + s(12), right - s(12))
                sy = rng.randint(top + s(18), bottom - s(18))
                draw.line((sx - s(22), sy, sx + s(24), sy + rng.randint(-s(5), s(5))), fill=(55, 125, 137), width=s(2))
            for side, connected in ((left, west), (right, east)):
                if not connected:
                    draw.line((side, top + s(6), side, bottom - s(6)), fill=(83, 87, 62), width=s(6))
            if not north:
                draw.line((left + s(5), top, right - s(5), top), fill=(91, 91, 60), width=s(6))
            if not south:
                draw.line((left + s(4), bottom, right - s(6), bottom), fill=(80, 83, 57), width=s(7))
            for _ in range(12):
                rx = rng.randint(left + s(8), right - s(8))
                ry = rng.randint(top + s(8), bottom - s(8))
                draw.ellipse((rx - s(5), ry - s(3), rx + s(6), ry + s(4)), fill=(85, 92, 80))
            if not (north or south or east or west):
                draw.ellipse((cx - s(24), cy - s(20), cx + s(24), cy + s(20)), fill=(44, 92, 103))


def draw_forests(draw: ImageDraw.ImageDraw, terrain: list[list[str]], rng: random.Random) -> None:
    for y, row in enumerate(terrain):
        for x, kind in enumerate(row):
            if kind != "forest":
                continue
            left, top, right, bottom = tile_box(x, y)
            for _ in range(28):
                px = rng.randint(left + s(12), right - s(12))
                py = rng.randint(top + s(14), bottom - s(12))
                trunk = jitter((78, 54, 34), 8, rng)
                draw.rectangle((px - s(3), py + s(7), px + s(3), py + s(23)), fill=trunk)
                for radius, color in ((24, (36, 74, 39)), (18, (54, 96, 42)), (12, (81, 120, 53))):
                    radius = s(radius)
                    ox = rng.randint(-s(4), s(4))
                    oy = rng.randint(-s(5), s(4))
                    draw.ellipse((px - radius + ox, py - radius + oy, px + radius + ox, py + radius + oy), fill=jitter(color, 8, rng))
                draw.ellipse((px - s(14), py - s(18), px + s(4), py - s(2)), fill=(103, 132, 56))
            for _ in range(28):
                px = rng.randint(left, right)
                py = rng.randint(top, bottom)
                draw.ellipse((px - s(3), py - s(3), px + s(3), py + s(3)), fill=(24, 54, 31))
            for _ in range(6):
                sx = rng.randint(left + s(20), right - s(20))
                sy = rng.randint(top + s(20), bottom - s(20))
                draw.arc((sx - s(26), sy - s(14), sx + s(28), sy + s(16)), 185, 335, fill=(22, 49, 29), width=s(2))


def draw_hills_and_cliffs(draw: ImageDraw.ImageDraw, terrain: list[list[str]], rng: random.Random) -> None:
    for y, row in enumerate(terrain):
        for x, kind in enumerate(row):
            if kind not in {"hill", "cliff"}:
                continue
            left, top, right, bottom = tile_box(x, y)
            if kind == "hill":
                for _ in range(10):
                    cx = rng.randint(left + s(22), right - s(22))
                    cy = rng.randint(top + s(22), bottom - s(22))
                    rx = rng.randint(s(20), s(42))
                    ry = rng.randint(s(10), s(25))
                    draw.ellipse((cx - rx, cy - ry, cx + rx, cy + ry), fill=jitter((126, 113, 80), 12, rng))
                    draw.arc((cx - rx, cy - ry, cx + rx, cy + ry), 180, 350, fill=(176, 160, 112), width=s(2))
                    draw.arc((cx - rx, cy - ry, cx + rx, cy + ry), 20, 160, fill=(72, 69, 52), width=s(2))
                for _ in range(12):
                    px = rng.randint(left + s(10), right - s(10))
                    py = rng.randint(top + s(10), bottom - s(10))
                    draw.line((px - s(11), py, px + s(13), py + rng.randint(-s(3), s(3))), fill=(89, 84, 62), width=s(1))
                continue

            draw.rectangle((left, top, right, bottom), fill=jitter((55, 52, 45), 5, rng))
            for _ in range(24):
                ax = rng.randint(left - s(8), right - s(12))
                ay = rng.randint(top + s(2), bottom - s(8))
                points = [
                    (ax, ay + rng.randint(s(12), s(26))),
                    (ax + rng.randint(s(14), s(31)), ay),
                    (ax + rng.randint(s(32), s(58)), ay + rng.randint(s(18), s(40))),
                    (ax + rng.randint(s(4), s(18)), ay + rng.randint(s(42), s(68))),
                ]
                draw.polygon(points, fill=jitter((75, 70, 58), 15, rng))
                draw.line(points + [points[0]], fill=(37, 37, 34), width=s(2))
            draw.line((left + s(8), top + s(5), right - s(8), bottom - s(8)), fill=(116, 105, 77), width=s(3))


def draw_structures(draw: ImageDraw.ImageDraw, terrain: list[list[str]], rng: random.Random) -> None:
    for y, row in enumerate(terrain):
        for x, kind in enumerate(row):
            if kind not in {"village", "camp", "fort"}:
                continue
            left, top, right, bottom = tile_box(x, y)
            cx, cy = tile_center(x, y)
            if kind == "village":
                draw_hut(draw, cx - s(24), cy - s(16), rng)
                draw_hut(draw, cx + s(24), cy + s(14), rng)
                draw.line((left + s(12), bottom - s(22), right - s(12), bottom - s(22)), fill=(91, 66, 39), width=s(5))
                draw.rectangle((cx - s(7), cy + s(22), cx + s(8), cy + s(45)), fill=(208, 165, 65))
                draw.line((cx - s(50), cy + s(45), cx + s(52), cy + s(35)), fill=(79, 61, 42), width=s(2))
                continue
            if kind == "camp":
                for ox, oy, color in ((-27, 6, (179, 129, 50)), (24, 0, (209, 174, 84))):
                    ox = s(ox)
                    oy = s(oy)
                    draw.polygon([(cx + ox, cy + oy - s(34)), (cx + ox - s(31), cy + oy + s(24)), (cx + ox + s(31), cy + oy + s(24))], fill=color)
                    draw.line((cx + ox, cy + oy - s(34), cx + ox, cy + oy + s(24)), fill=(88, 58, 32), width=s(3))
                    draw.line((cx + ox - s(31), cy + oy + s(24), cx + ox + s(31), cy + oy + s(24)), fill=(92, 61, 35), width=s(4))
                    draw.line((cx + ox - s(17), cy + oy + s(10), cx + ox + s(15), cy + oy + s(10)), fill=(225, 189, 92), width=s(2))
                draw.line((left + s(12), top + s(20), right - s(12), top + s(10)), fill=(105, 81, 45), width=s(4))
                draw_banner(draw, right - s(28), top + s(28), (216, 171, 57))
                continue
            draw.rectangle((left + s(12), top + s(22), right - s(12), bottom - s(18)), outline=(73, 55, 42), width=s(8))
            draw.rectangle((left + s(20), top + s(30), right - s(20), bottom - s(26)), outline=(119, 88, 60), width=s(5))
            draw.rectangle((cx - s(18), bottom - s(50), cx + s(18), bottom - s(18)), fill=(63, 48, 39))
            for tx, ty in ((left + s(16), top + s(20)), (right - s(16), top + s(20)), (left + s(16), bottom - s(18)), (right - s(16), bottom - s(18))):
                draw.rectangle((tx - s(10), ty - s(10), tx + s(10), ty + s(10)), fill=(93, 70, 52))
            draw.line((left + s(18), cy, right - s(18), cy), fill=(83, 62, 48), width=s(3))
            draw_banner(draw, cx + s(26), top + s(28), (213, 171, 54))


def draw_hut(draw: ImageDraw.ImageDraw, x: int, y: int, rng: random.Random) -> None:
    draw.rectangle((x - s(22), y - s(2), x + s(22), y + s(26)), fill=jitter((106, 74, 45), 8, rng))
    draw.polygon([(x - s(31), y), (x, y - s(28)), (x + s(31), y)], fill=jitter((151, 106, 48), 8, rng))
    draw.line((x - s(30), y, x + s(30), y), fill=(84, 56, 34), width=s(4))
    draw.rectangle((x - s(7), y + s(8), x + s(6), y + s(26)), fill=(55, 39, 31))
    draw.line((x - s(20), y + s(8), x + s(20), y + s(8)), fill=(137, 93, 52), width=s(2))


def draw_banner(draw: ImageDraw.ImageDraw, x: int, y: int, color: tuple[int, int, int]) -> None:
    draw.line((x, y - s(22), x, y + s(30)), fill=(68, 48, 31), width=s(3))
    draw.polygon([(x, y - s(18)), (x + s(23), y - s(12)), (x, y + s(2))], fill=color)
    draw.line((x, y - s(18), x + s(23), y - s(12), x, y + s(2)), fill=(96, 66, 28), width=s(1))


def draw_shadows_and_light(image: Image.Image, rng: random.Random) -> Image.Image:
    width, height = image.size
    shade = Image.new("RGBA", image.size, (0, 0, 0, 0))
    draw = ImageDraw.Draw(shade)
    for _ in range(260):
        x = rng.randint(-s(80), width)
        y = rng.randint(-s(60), height)
        rx = rng.randint(s(45), s(170))
        ry = rng.randint(s(20), s(95))
        draw.ellipse((x - rx, y - ry, x + rx, y + ry), fill=(0, 0, 0, rng.randint(5, 16)))
    shade = shade.filter(ImageFilter.GaussianBlur(s(18)))
    result = Image.alpha_composite(image.convert("RGBA"), shade)

    glow = Image.new("RGBA", image.size, (0, 0, 0, 0))
    gdraw = ImageDraw.Draw(glow)
    gdraw.ellipse((-width * 0.22, -height * 0.22, width * 0.82, height * 0.62), fill=(255, 213, 135, 28))
    glow = glow.filter(ImageFilter.GaussianBlur(s(80)))
    result = Image.alpha_composite(result, glow)

    mask_size = (320, max(1, round(320 * height / width)))
    vignette_mask = Image.new("L", mask_size, 0)
    pixels = vignette_mask.load()
    cx = (mask_size[0] - 1) / 2
    cy = (mask_size[1] - 1) / 2
    max_distance = math.hypot(cx, cy)
    for py in range(mask_size[1]):
        for px in range(mask_size[0]):
            distance = math.hypot(px - cx, py - cy) / max_distance
            pixels[px, py] = clamp(round(max(0, distance - 0.45) / 0.55 * 68))
    vignette_mask = vignette_mask.resize(image.size, Image.Resampling.BICUBIC).filter(ImageFilter.GaussianBlur(s(18)))
    vignette = Image.new("RGBA", image.size, (0, 0, 0, 0))
    vignette.putalpha(vignette_mask)
    result = Image.alpha_composite(result, vignette)
    return result.convert("RGB")


def add_tile_seams(image: Image.Image, width: int, height: int) -> Image.Image:
    overlay = Image.new("RGBA", image.size, (0, 0, 0, 0))
    draw = ImageDraw.Draw(overlay)
    for x in range(1, width):
        px = x * TILE
        draw.line((px, 0, px, height * TILE), fill=(14, 19, 14, 10), width=s(1))
    for y in range(1, height):
        py = y * TILE
        draw.line((0, py, width * TILE, py), fill=(14, 19, 14, 10), width=s(1))
    return Image.alpha_composite(image.convert("RGBA"), overlay).convert("RGB")


def main() -> None:
    rng = random.Random(SEED)
    width, height, terrain = load_first_battle_map()
    image = draw_base(width, height, terrain, rng)
    draw = ImageDraw.Draw(image)
    draw_roads(draw, terrain, rng)
    draw_rivers(draw, terrain, rng)
    draw_hills_and_cliffs(draw, terrain, rng)
    draw_forests(draw, terrain, rng)
    draw_grass_detail(draw, terrain, rng)
    draw_structures(draw, terrain, rng)
    image = draw_shadows_and_light(image, rng)
    image = add_tile_seams(image, width, height)
    OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
    image.save(OUTPUT_PATH, optimize=True)
    print(f"Generated {OUTPUT_PATH} ({image.width}x{image.height})")


if __name__ == "__main__":
    main()
