#!/usr/bin/env python3
"""
Generate procedural 1K PBR texture maps for the Tactical Shooter modular art kit.
Produces: basecolor, normal, roughness, metallic — all at 1024x1024.
Usage: python3 generate_textures.py
"""

import math
import os
import random
from PIL import Image, ImageFilter, ImageChops, ImageDraw

TEXTURE_DIR = os.path.expanduser("/home/oplabs/tactical-shooter/client/assets/textures")
SIZE = 1024

random.seed(42)

def save_pbr(base_dir, name, basecolor, normal, roughness, metallic):
    """Save all PBR maps for one material."""
    d = os.path.join(base_dir, name)
    os.makedirs(d, exist_ok=True)
    basecolor.save(os.path.join(d, "basecolor.png"))
    normal.save(os.path.join(d, "normal.png"))
    roughness.save(os.path.join(d, "roughness.png"))
    metallic.save(os.path.join(d, "metallic.png"))
    print(f"  Generated {name}/ — 4 maps saved")


# ── Utility helpers ──────────────────────────────────────────────────────

def gaussian_noise(size, scale=1.0, seed=None):
    """Generate a grayscale image with gaussian noise."""
    if seed is not None:
        random.seed(seed)
    img = Image.new("L", (size, size))
    pix = img.load()
    for y in range(size):
        for x in range(size):
            v = int(random.gauss(128, 64 * scale))
            pix[x, y] = max(0, min(255, v))
    return img


def tile_blur(img, radius):
    """Blur with tileable boundary by mirroring edges before blur."""
    w, h = img.size
    big = Image.new(img.mode, (w * 3, h * 3))
    for dx in range(3):
        for dy in range(3):
            if dx == 1 and dy == 1:
                big.paste(img, (w, h))
            else:
                # Mirror the tile
                mirror = img.transpose(Image.FLIP_LEFT_RIGHT) if dx % 2 == 0 else img
                if dy % 2 == 0:
                    mirror = mirror.transpose(Image.FLIP_TOP_BOTTOM)
                big.paste(mirror, (w * dx, h * dy))
    big = big.filter(ImageFilter.GaussianBlur(radius))
    return big.crop((w, h, w * 2, h * 2))


def normal_from_height(height_img, strength=2.0):
    """Generate an RGB normal map from a grayscale height map using Sobel."""
    w, h = height_img.size
    # Get pixel data
    hpix = height_img.load()
    normal = Image.new("RGB", (w, h))
    npix = normal.load()
    for y in range(h):
        for x in range(w):
            # Sample height with wrapping for tileability
            sx1 = hpix[(x - 1) % w, y]
            sx2 = hpix[(x + 1) % w, y]
            sy1 = hpix[x, (y - 1) % h]
            sy2 = hpix[x, (y + 1) % h]
            dx = (sx2 - sx1) / 255.0
            dy = (sy2 - sy1) / 255.0
            dz = 1.0 / strength
            length = math.sqrt(dx * dx + dy * dy + dz * dz)
            nx = dx / length
            ny = dy / length
            nz = dz / length
            # Map from [-1,1] to [0,255]
            npix[x, y] = (
                int((nx * 0.5 + 0.5) * 255),
                int((ny * 0.5 + 0.5) * 255),
                int((nz * 0.5 + 0.5) * 255),
            )
    return normal


def make_cellular(size, cell_size=32, seed=None):
    """Generate a cellular/Voronoi-like pattern as grayscale."""
    if seed is not None:
        random.seed(seed)
    img = Image.new("L", (size, size))
    pix = img.load()

    # Generate cell centers with random heights
    cells = {}
    for cx in range(0, size + cell_size, cell_size):
        for cy in range(0, size + cell_size, cell_size):
            ox = random.randint(0, cell_size - 1)
            oy = random.randint(0, cell_size - 1)
            h = random.randint(40, 200)
            cells[(cx + ox, cy + oy)] = h

    for y in range(size):
        for x in range(size):
            min_dist = float("inf")
            best_h = 128
            for (cx, cy), h in cells.items():
                d = (cx - x) ** 2 + (cy - y) ** 2
                if d < min_dist:
                    min_dist = d
                    best_h = h
            pix[x, y] = best_h
    return img


def concrete_base(size, base_rgb=(180, 175, 168), noise_scale=1.0):
    """Generate concrete-like basecolor with aggregate variation."""
    # Start with light noise
    noise = gaussian_noise(size, scale=0.15, seed=42)
    noise = tile_blur(noise, 4)
    
    # Add larger variation
    big = gaussian_noise(size, scale=0.08, seed=43)
    big = tile_blur(big, 24)
    
    # Add some darker speckles (aggregate)
    speckle = Image.new("L", (size, size), 0)
    spix = speckle.load()
    for _ in range(800):
        x = random.randint(0, size - 1)
        y = random.randint(0, size - 1)
        spix[x, y] = random.randint(30, 80)
    speckle = tile_blur(speckle, 1.5)
    
    # Combine
    combined = Image.new("L", (size, size))
    cpix = combined.load()
    npix = noise.load()
    bpix = big.load()
    skpix = speckle.load()
    for y in range(size):
        for x in range(size):
            v = npix[x, y] + bpix[x, y] - 128 + skpix[x, y] - 30
            cpix[x, y] = max(0, min(255, v))
    
    # Tint and apply
    result = Image.new("RGB", (size, size))
    rpix = result.load()
    cp = combined.load()
    for y in range(size):
        for x in range(size):
            f = cp[x, y] / 255.0
            rpix[x, y] = (
                max(0, min(255, int(base_rgb[0] * f / 180))),
                max(0, min(255, int(base_rgb[1] * f / 175))),
                max(0, min(255, int(base_rgb[2] * f / 168))),
            )
    return result


# ── Material definitions ─────────────────────────────────────────────────

def generate_wall_concrete_01():
    """Light clean concrete — primary wall material."""
    size = SIZE
    # Basecolor
    bc = concrete_base(size, base_rgb=(200, 195, 185))
    bc = bc.filter(ImageFilter.SMOOTH)
    
    # Height map for normal (slight texture)
    height = gaussian_noise(size, scale=0.1, seed=100)
    height = tile_blur(height, 3)
    normal = normal_from_height(height, strength=1.5)
    
    # Roughness — concrete is fairly rough
    rough = gaussian_noise(size, scale=0.08, seed=200)
    rough = tile_blur(rough, 6)
    # Brighten a bit (rougher = lighter in roughness map)
    rpix = rough.load()
    for y in range(size):
        for x in range(size):
            rpix[x, y] = min(255, rpix[x, y] + 40)
    
    # Metallic — none
    metallic = Image.new("L", (size, size), 0)
    
    save_pbr(TEXTURE_DIR, "wall_concrete_01", bc, normal, rough, metallic)


def generate_wall_concrete_02():
    """Darker, smoother concrete — accent walls."""
    size = SIZE
    bc = concrete_base(size, base_rgb=(145, 140, 135))
    bc = bc.filter(ImageFilter.SMOOTH_MORE)
    
    height = gaussian_noise(size, scale=0.06, seed=101)
    height = tile_blur(height, 5)
    normal = normal_from_height(height, strength=1.2)
    
    rough = gaussian_noise(size, scale=0.06, seed=201)
    rough = tile_blur(rough, 8)
    rpix = rough.load()
    for y in range(size):
        for x in range(size):
            rpix[x, y] = min(255, rpix[x, y] + 30)
    
    metallic = Image.new("L", (size, size), 0)
    save_pbr(TEXTURE_DIR, "wall_concrete_02", bc, normal, rough, metallic)


def generate_floor_tile_01():
    """Checkerboard tile floor — interior spaces."""
    size = SIZE
    tile_size = 64  # 64px tiles at 1024 = 16 tiles across
    
    bc = Image.new("RGB", (size, size))
    bpix = bc.load()
    for y in range(size):
        for x in range(size):
            tx = x // tile_size
            ty = y // tile_size
            if (tx + ty) % 2 == 0:
                # Light tile
                v = 200 + random.randint(-10, 10)
                bpix[x, y] = (v, v - 5, v - 10)
            else:
                # Dark tile
                v = 160 + random.randint(-8, 8)
                bpix[x, y] = (v, v - 3, v - 5)
    
    # Add grout lines
    draw = ImageDraw.Draw(bc)
    for i in range(0, size + 1, tile_size):
        draw.line([(i, 0), (i, size)], fill=(70, 65, 60), width=2)
        draw.line([(0, i), (size, i)], fill=(70, 65, 60), width=2)
    
    # Height map for normal
    height = Image.new("L", (size, size), 128)
    hpix = height.load()
    for y in range(size):
        for x in range(size):
            tx = x // tile_size
            ty = y // tile_size
            gx = x % tile_size
            gy = y % tile_size
            # Slight bevel around edges (tile center higher)
            d = min(gx, tile_size - gx, gy, tile_size - gy)
            if d < 3:
                hpix[x, y] = 100  # Grout is lower
            else:
                hpix[x, y] = 140 + (d % 4) * 5
    
    # Apply bevel and tile variation
    hpix = height.load()
    for y in range(size):
        for x in range(size):
            tx = x // tile_size
            ty = y // tile_size
            gx = x % tile_size
            gy = y % tile_size
            d_edge = min(gx, tile_size - gx, gy, tile_size - gy)
            if d_edge < 2:
                hpix[x, y] = 90
            elif d_edge < 5:
                hpix[x, y] = 100 + d_edge * 8
            else:
                # Gentle tile surface variation
                hpix[x, y] = 140 + ((tx * 13 + ty * 7 + x + y) % 8) * 3
    
    normal = normal_from_height(height, strength=3.0)
    
    # Roughness — tiles are smooth
    rough = Image.new("L", (size, size), 80)
    rpix = rough.load()
    for y in range(size):
        for x in range(size):
            tx = x // tile_size
            ty = y // tile_size
            gx = x % tile_size
            gy = y % tile_size
            d_edge = min(gx, tile_size - gx, gy, tile_size - gy)
            if d_edge < 2:
                rpix[x, y] = 200  # Grout is rough
            else:
                rpix[x, y] = 60 + ((tx * 17 + ty * 11) % 10) * 4
    
    metallic = Image.new("L", (size, size), 0)
    save_pbr(TEXTURE_DIR, "floor_tile_01", bc, normal, rough, metallic)


def generate_floor_concrete_01():
    """Industrial dark concrete floor — warehouse/industrial areas."""
    size = SIZE
    
    # Base
    bc = concrete_base(size, base_rgb=(130, 128, 123))
    bc = bc.filter(ImageFilter.SMOOTH)
    
    # Add surface scratches/wear marks
    draw = ImageDraw.Draw(bc)
    for _ in range(50):
        x1 = random.randint(0, size)
        y1 = random.randint(0, size)
        length = random.randint(10, 100)
        angle = random.uniform(0, math.pi * 2)
        x2 = x1 + int(math.cos(angle) * length)
        y2 = y1 + int(math.sin(angle) * length)
        c = random.randint(100, 140)
        draw.line([(x1, y1), (x2, y2)], fill=(c, c - 3, c - 5), width=1)
    
    height = gaussian_noise(size, scale=0.15, seed=102)
    height = tile_blur(height, 3)
    hpix = height.load()
    # Add groove lines (expansion joints)
    for y in range(size):
        for x in range(size):
            gx = x % 256
            gy = y % 256
            if min(gx, 256 - gx) < 2 or min(gy, 256 - gy) < 2:
                hpix[x, y] = max(0, hpix[x, y] - 40)
    normal = normal_from_height(height, strength=2.5)
    
    rough = Image.new("L", (size, size), 160)
    rpix = rough.load()
    for y in range(size):
        for x in range(size):
            gx = x % 256
            gy = y % 256
            if min(gx, 256 - gx) < 2 or min(gy, 256 - gy) < 2:
                rpix[x, y] = 220  # Grooves are rougher
            else:
                rpix[x, y] = 150 + (hash((x // 8, y // 8)) % 20)
    
    metallic = Image.new("L", (size, size), 0)
    save_pbr(TEXTURE_DIR, "floor_concrete_01", bc, normal, rough, metallic)


def generate_metal_structural_01():
    """Structural metal — beams, supports, vents."""
    size = SIZE
    # Base — grey metallic
    bc = Image.new("RGB", (size, size))
    bpix = bc.load()
    for y in range(size):
        for x in range(size):
            v = 160 + int(random.gauss(0, 4))
            bpix[x, y] = (max(0, min(255, v)), max(0, min(255, v - 2)), max(0, min(255, v - 4)))
    
    # Add horizontal brush marks
    draw = ImageDraw.Draw(bc)
    for _ in range(200):
        bx = random.randint(0, size - 1)
        by = random.randint(0, size - 1)
        c = random.randint(-6, 6)
        draw.line([(bx, by), (bx + random.randint(10, 60), by)], fill=(bpix[bx, by][0] + c,) * 3, width=1)
    
    # Height map for normal
    height = gaussian_noise(size, scale=0.05, seed=103)
    height = tile_blur(height, 2)
    hpix = height.load()
    for y in range(size):
        for x in range(size):
            hpix[x, y] = max(0, min(255, hpix[x, y] - 10))  # Flatter than concrete
    normal = normal_from_height(height, strength=1.0)  # Subtle
    
    # Roughness — smooth metal
    rough = Image.new("L", (size, size), 40)
    rpix = rough.load()
    for y in range(size):
        for x in range(size):
            hp = hpix[x, y]
            rpix[x, y] = 30 + (hp - 128) // 8
    
    # Metallic — yes!
    metallic = Image.new("L", (size, size), 255)
    save_pbr(TEXTURE_DIR, "metal_structural_01", bc, normal, rough, metallic)


def generate_accent_team_blue():
    """Blue team accent — CT side colored panels."""
    size = SIZE
    # Deep blue
    bc = Image.new("RGB", (size, size))
    bpix = bc.load()
    for y in range(size):
        for x in range(size):
            noise = random.randint(-8, 8)
            bpix[x, y] = (
                max(0, min(255, 40 + noise)),
                max(0, min(255, 80 + noise)),
                max(0, min(255, 200 + noise)),
            )
    bc = bc.filter(ImageFilter.SMOOTH)
    # Slight brushed pattern
    draw = ImageDraw.Draw(bc)
    for y in range(0, size, 4):
        v = random.randint(-4, 4)
        draw.line([(0, y), (size, y)], fill=(40 + v, 80 + v, 200 + v), width=1)
    
    height = Image.new("L", (size, size), 128)
    hpix = height.load()
    for y in range(size):
        for x in range(size):
            hpix[x, y] = 128 + int(random.gauss(0, 3))
    hpix = height.load()
    for y in range(0, size, 4):
        for x in range(size):
            hpix[x, y] = 110  # Slight panel line
    normal = normal_from_height(height, strength=0.8)
    
    rough = Image.new("L", (size, size), 50)
    rpix = rough.load()
    for y in range(size):
        for x in range(size):
            rpix[x, y] = 40 + (hash((x // 16, y // 16)) % 15)
    
    # Slightly metallic (painted metal)
    metallic = Image.new("L", (size, size), 128)
    save_pbr(TEXTURE_DIR, "accent_team_blue", bc, normal, rough, metallic)


def generate_accent_team_red():
    """Red/orange team accent — T side colored panels."""
    size = SIZE
    bc = Image.new("RGB", (size, size))
    bpix = bc.load()
    for y in range(size):
        for x in range(size):
            noise = random.randint(-8, 8)
            bpix[x, y] = (
                max(0, min(255, 200 + noise)),
                max(0, min(255, 60 + noise)),
                max(0, min(255, 30 + noise)),
            )
    bc = bc.filter(ImageFilter.SMOOTH)
    draw = ImageDraw.Draw(bc)
    for y in range(0, size, 4):
        v = random.randint(-4, 4)
        draw.line([(0, y), (size, y)], fill=(200 + v, 60 + v, 30 + v), width=1)
    
    height = Image.new("L", (size, size), 128)
    hpix = height.load()
    for y in range(size):
        for x in range(size):
            hpix[x, y] = 128 + int(random.gauss(0, 3))
    for y in range(0, size, 4):
        for x in range(size):
            hpix[x, y] = 110
    normal = normal_from_height(height, strength=0.8)
    
    rough = Image.new("L", (size, size), 50)
    rpix = rough.load()
    for y in range(size):
        for x in range(size):
            rpix[x, y] = 40 + (hash((x // 16, y // 16)) % 15)
    
    metallic = Image.new("L", (size, size), 128)
    save_pbr(TEXTURE_DIR, "accent_team_red", bc, normal, rough, metallic)


# ── Main ────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    os.makedirs(TEXTURE_DIR, exist_ok=True)
    print("Generating PBR textures (1K)…")
    
    print("  [1/7] wall_concrete_01")
    generate_wall_concrete_01()
    
    print("  [2/7] wall_concrete_02")
    generate_wall_concrete_02()
    
    print("  [3/7] floor_tile_01")
    generate_floor_tile_01()
    
    print("  [4/7] floor_concrete_01")
    generate_floor_concrete_01()
    
    print("  [5/7] metal_structural_01")
    generate_metal_structural_01()
    
    print("  [6/7] accent_team_blue")
    generate_accent_team_blue()
    
    print("  [7/7] accent_team_red")
    generate_accent_team_red()
    
    print("\nDone — 7 materials × 4 maps = 28 texture files generated at 1K.")
