"""
Render perspective illusion video — faithful to original GeoGebra post.
"""
from PIL import Image, ImageDraw, ImageFilter
import numpy as np, os, subprocess, math, shutil

# ── Canvas ──────────────────────────────────────────────────────────────
W, H    = 820, 480
FPS     = 30
SECS    = 10
N       = FPS * SECS
FRAMES  = "/Users/bowang/.openclaw/workspace/_frames3"
OUT     = "/Users/bowang/.openclaw/workspace/perspective-illusion-demo.mp4"
SPRITE  = "/Users/bowang/.openclaw/workspace/_figure_sprite.png"

os.makedirs(FRAMES, exist_ok=True)

# ── Geometry ─────────────────────────────────────────────────────────────
ground_y = int(H * 0.77)       # red baseline y

# Diagonal: from upper-right corner to lower-left (past canvas)
diag_start = (W + 10, int(H * 0.02))     # upper right
diag_end   = (-60,    ground_y + 2)       # lower left — meets baseline

def diag_y_at(x):
    """Y value of diagonal at given x (linear interp)."""
    t = (x - diag_end[0]) / (diag_start[0] - diag_end[0])
    return diag_end[1] + t * (diag_start[1] - diag_end[1])

def fig_height(x):
    """Height of figure at x = gap between diagonal and baseline."""
    return max(4, ground_y - diag_y_at(x))

# Draggable dot x range
dot_xmin = 90
dot_xmax = W - 55

# Ghost positions
ghost_xs = [140, 255, 400, 565]

# ── Sprite loading ────────────────────────────────────────────────────────
raw_sprite = Image.open(SPRITE).convert("RGBA")
# Find tight bounding box of the figure (non-white pixels)
arr = np.array(raw_sprite)
# Threshold: pixels where R < 200 (dark = figure lines)
mask = arr[:,:,0] < 210
rows = np.any(mask, axis=1)
cols = np.any(mask, axis=0)
if rows.any():
    rmin, rmax = np.where(rows)[0][[0,-1]]
    cmin, cmax = np.where(cols)[0][[0,-1]]
    cropped = raw_sprite.crop((cmin, rmin, cmax+1, rmax+1))
else:
    cropped = raw_sprite

# Make white → transparent
def white_to_alpha(img):
    img = img.convert("RGBA")
    data = np.array(img, dtype=float)
    # Alpha = how dark the pixel is (dark lines = opaque, white = transparent)
    r, g, b = data[:,:,0], data[:,:,1], data[:,:,2]
    lightness = (r + g + b) / 3.0
    # Invert: dark pixels become fully opaque
    alpha = np.clip((255 - lightness) * 1.8, 0, 255).astype(np.uint8)
    data[:,:,3] = alpha
    return Image.fromarray(data.astype(np.uint8), "RGBA")

figure_sprite = white_to_alpha(cropped)
SPRITE_W, SPRITE_H = figure_sprite.size  # aspect ratio reference

def get_figure(target_height, alpha_scale=1.0):
    """Return the figure sprite scaled to target_height, with alpha."""
    if target_height < 6:
        return None
    asp = SPRITE_W / SPRITE_H
    tw  = max(1, int(target_height * asp))
    th  = max(1, int(target_height))
    scaled = figure_sprite.resize((tw, th), Image.LANCZOS)
    if alpha_scale < 1.0:
        arr = np.array(scaled, dtype=np.float32)
        arr[:,:,3] *= alpha_scale
        scaled = Image.fromarray(arr.astype(np.uint8), "RGBA")
    return scaled

# ── Draw one frame ─────────────────────────────────────────────────────────
def draw_frame(t_norm):
    img = Image.new("RGBA", (W, H), (255, 255, 255, 255))
    draw = ImageDraw.Draw(img)

    # Diagonal line (thin gray)
    draw.line([diag_start, diag_end], fill=(100, 95, 88, 100), width=1)

    # Red baseline
    draw.line([(0, ground_y), (W, ground_y)], fill=(200, 40, 40, 255), width=2)

    # Current dot x
    dx = int(dot_xmin + (dot_xmax - dot_xmin) * t_norm)

    # Ghost figures
    for gx in ghost_xs:
        if abs(gx - dx) > 25:
            fh = fig_height(gx)
            fig = get_figure(fh, alpha_scale=0.22)
            if fig:
                fw = fig.size[0]
                # Paste figure so feet sit on baseline
                px = gx - fw // 2
                py = ground_y - fig.size[1]
                img.paste(fig, (px, py), fig)

    # Main figure
    fh  = fig_height(dx)
    fig = get_figure(fh, alpha_scale=1.0)
    if fig:
        fw = fig.size[0]
        px = dx - fw // 2
        py = ground_y - fig.size[1]
        img.paste(fig, (px, py), fig)

    # Left fixed dot
    r = 6
    lx = dot_xmin
    draw.ellipse([(lx-r, ground_y-r), (lx+r, ground_y+r)],
                 fill=(41, 128, 185, 255))

    # Draggable dot (with outer ring)
    R = 8
    draw.ellipse([(dx-R, ground_y-R), (dx+R, ground_y+R)],
                 fill=(41, 128, 185, 255), outline=(255,255,255,255), width=2)
    draw.ellipse([(dx-R-5, ground_y-R-5), (dx+R+5, ground_y+R+5)],
                 outline=(41, 128, 185, 130), width=2)

    return img.convert("RGB")

# ── Animation curve ────────────────────────────────────────────────────────
def ease_in_out(x):
    return x*x*(3 - 2*x)

# ── Render frames ──────────────────────────────────────────────────────────
print(f"Rendering {N} frames...")
for i in range(N):
    phase = (i / N) * 2  # 0→2 (forward + back)
    raw   = phase if phase < 1 else 2 - phase
    t     = 0.04 + ease_in_out(raw) * 0.92
    frame = draw_frame(t)
    frame.save(f"{FRAMES}/f_{i:04d}.png")
    if i % 60 == 0:
        print(f"  {i}/{N}")

# ── Encode ─────────────────────────────────────────────────────────────────
print("Encoding...")
subprocess.run([
    "ffmpeg", "-y",
    "-framerate", str(FPS),
    "-i", f"{FRAMES}/f_%04d.png",
    "-vf", "scale=820:480",
    "-c:v", "libx264",
    "-pix_fmt", "yuv420p",
    "-crf", "16",
    "-preset", "fast",
    OUT
], check=True)

shutil.rmtree(FRAMES)
print(f"Done → {OUT}")
