#!/usr/bin/env python3
"""
Generate a pool of realistic phone-sized JPEGs for the EventSnap load test.

The stress test uploads ~1000 images, but they don't need to be 1000 unique
files — real load comes from realistic *byte size* and *decode cost*, which
drive bandwidth, the compression/preview pipeline (decode + 800x800 resize),
disk usage and the dynamic storage quota. So we generate a modest POOL of
distinct, high-entropy images (~2-4 MB, 12 MP, like a phone camera) and the
driver reuses them at random across the 1000 uploads.

High entropy matters: a flat gradient compresses to almost nothing and would
under-stress both the network and the JPEG decoder. We blend random noise over
a colorful gradient + big shapes so the files land in a realistic size band,
then tune JPEG quality per-image to hit the target size.

Output: $LT_PHOTOS_DIR (default /tmp/eventsnap-loadtest/photos)
Usage:  python3 gen-images.py [COUNT]     (default 40)
"""

import os
import sys
import math
import random
from PIL import Image, ImageDraw, ImageFont

OUT_DIR = os.environ.get("LT_PHOTOS_DIR", "/tmp/eventsnap-loadtest/photos")
COUNT = int(sys.argv[1]) if len(sys.argv) > 1 else 40

# Target JPEG size band (bytes). Typical modern phone photo.
TARGET_MIN = 2_000_000
TARGET_MAX = 4_500_000

# 12 MP-ish, both orientations (phones shoot portrait and landscape).
SIZES = [(4032, 3024), (3024, 4032)]

PALETTES = [
    [(250, 245, 235), (212, 175, 55), (120, 90, 30)],   # champagne / gold
    [(245, 244, 242), (190, 190, 198), (90, 92, 100)],   # silver / pearl
    [(255, 250, 240), (240, 200, 160), (170, 110, 70)],  # warm sunset
    [(235, 240, 248), (140, 170, 210), (40, 70, 120)],   # cool blue hour
    [(248, 240, 245), (210, 150, 180), (110, 50, 90)],   # rose dusk
    [(240, 248, 242), (150, 200, 170), (40, 110, 80)],   # garden green
]


def lerp(a, b, t):
    return tuple(int(a[i] + (b[i] - a[i]) * t) for i in range(3))


def gradient(w, h, palette, rng):
    """Diagonal 3-stop gradient base."""
    base = Image.new("RGB", (w, h))
    px = base.load()
    ang = rng.uniform(0, math.pi)
    dx, dy = math.cos(ang), math.sin(ang)
    # precompute per-column/row projection for speed
    maxproj = abs(dx) * w + abs(dy) * h
    for y in range(h):
        for x in range(0, w, 4):  # step 4 then fill — good enough, much faster
            t = (dx * x + dy * y) / maxproj
            t = min(1.0, max(0.0, t + rng.uniform(-0.02, 0.02)))
            if t < 0.5:
                c = lerp(palette[0], palette[1], t * 2)
            else:
                c = lerp(palette[1], palette[2], (t - 0.5) * 2)
            for k in range(4):
                if x + k < w:
                    px[x + k, y] = c
    return base


def add_shapes(img, rng):
    d = ImageDraw.Draw(img, "RGBA")
    w, h = img.size
    for _ in range(rng.randint(6, 14)):
        x0 = rng.randint(-w // 5, w)
        y0 = rng.randint(-h // 5, h)
        r = rng.randint(w // 12, w // 3)
        col = (rng.randint(0, 255), rng.randint(0, 255), rng.randint(0, 255), rng.randint(20, 90))
        if rng.random() < 0.5:
            d.ellipse([x0, y0, x0 + r, y0 + r], fill=col)
        else:
            d.rectangle([x0, y0, x0 + r, y0 + int(r * rng.uniform(0.4, 1.6))], fill=col)
    return img


def noisy(w, h, rng):
    """Full-resolution RGB noise from urandom — maximum entropy."""
    return Image.frombytes("RGB", (w, h), os.urandom(w * h * 3))


def label(img, idx, rng):
    d = ImageDraw.Draw(img)
    txt = f"EventSnap load #{idx:03d}"
    try:
        font = ImageFont.load_default(size=64)
    except TypeError:
        font = ImageFont.load_default()
    d.text((60, 60), txt, fill=(255, 255, 255), font=font)
    d.text((62, 62), txt, fill=(0, 0, 0), font=font)  # cheap shadow offset


def encode_to_band(img, path, rng):
    """Try qualities high→low until the file lands under TARGET_MAX; keep the
    first that also clears TARGET_MIN if possible."""
    best = None
    for q in (92, 88, 84, 80, 76, 72):
        img.save(path, "JPEG", quality=q, optimize=False)
        size = os.path.getsize(path)
        best = (q, size)
        if size <= TARGET_MAX:
            if size >= TARGET_MIN:
                return q, size
            # under the band — noise alpha likely too low; accept anyway at high q
            return q, size
    return best  # even q72 too big; accept the smallest we made


def main():
    os.makedirs(OUT_DIR, exist_ok=True)
    rng = random.Random(20260718)  # deterministic pool
    total_bytes = 0
    print(f"[gen] writing {COUNT} images to {OUT_DIR}")
    for i in range(COUNT):
        w, h = rng.choice(SIZES)
        palette = rng.choice(PALETTES)
        base = gradient(w, h, palette, rng)
        base = add_shapes(base, rng)
        # blend noise to inject entropy -> realistic JPEG size
        alpha = rng.uniform(0.28, 0.42)
        base = Image.blend(base, noisy(w, h, rng), alpha)
        label(base, i, rng)
        path = os.path.join(OUT_DIR, f"photo_{i:03d}.jpg")
        q, size = encode_to_band(base, path, rng)
        total_bytes += size
        print(f"  photo_{i:03d}.jpg  {w}x{h}  q{q}  {size/1_000_000:.2f} MB")
    avg = total_bytes / COUNT
    print(f"[gen] done. {COUNT} images, avg {avg/1_000_000:.2f} MB, "
          f"pool total {total_bytes/1_000_000:.1f} MB")
    print(f"[gen] projected for 1000 uploads (originals only): "
          f"~{avg*1000/1_000_000_000:.1f} GB")


if __name__ == "__main__":
    main()
