"""
GPU Benchmark Animation for @fminxyz Telegram Channel
1080x1080 px, 12 fps, 25 seconds (= 300 frames)

Four acts:
  Act 1 (0-7s):   CPU vs GPU timeline — async kernel dispatch
  Act 2 (7-14s):  Wrong way: time.time() measuring CPU dispatch only
  Act 3 (14-21s): Right way: cuda.Event with synchronize
  Act 4 (21-25s): Checklist — warmup, synchronize, median, eval mode
"""

import os
import sys
import tempfile
import subprocess
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyArrowPatch, FancyBboxPatch
import warnings
warnings.filterwarnings('ignore')

OUTPUT = "/root/Strategy/content/drafts/gpu_benchmark_animation.mp4"
FPS    = 12
DUR    = 25
NFRAM  = FPS * DUR   # 300
DPI    = 100
FW, FH = 10.8, 10.8

BG   = "#1a1a2e"
PAN  = "#16213e"
CYAN = "#00d4ff"
PINK = "#ff6b9d"
TXT  = "#e8e8f0"
ACC  = "#a8a8ff"
GRD  = "#2a2a4a"
YELL = "#ffe066"
GRN  = "#4ade80"
RED  = "#ff4444"
ORG  = "#ff9500"

rng = np.random.default_rng(42)

A1S, A1E = 0,        7 * FPS    # 0..84
A2S, A2E = 7 * FPS,  14 * FPS   # 84..168
A3S, A3E = 14 * FPS, 21 * FPS   # 168..252
A4S, A4E = 21 * FPS, NFRAM      # 252..300
BLEND     = 4


def cl(v, lo=0.0, hi=1.0):
    return float(np.clip(float(v), lo, hi - 1e-9))


def fp(local, s, e, dur):
    a = s * dur; b = e * dur
    return cl((local - a) / max(1, b - a))


def ease_in_out(t):
    """Smooth easing function."""
    return t * t * (3 - 2 * t)


def draw_rounded_rect(ax, x, y, w, h, color, alpha=1.0, lw=0, edgecolor=None, zorder=2):
    """Draw a rounded rectangle."""
    ec = edgecolor if edgecolor else color
    rect = FancyBboxPatch((x, y), w, h,
                          boxstyle="round,pad=0.01",
                          facecolor=color, edgecolor=ec,
                          alpha=alpha, lw=lw, zorder=zorder)
    ax.add_patch(rect)
    return rect


def make_frame(frame, fig, axes):
    ax_main, axT, axB = axes
    fig.patch.set_facecolor(BG)

    def hide():
        ax_main.cla()
        ax_main.set_facecolor(PAN)
        ax_main.axis('off')
        ax_main.set_xlim(0, 1)
        ax_main.set_ylim(0, 1)
        for a in [axT, axB]:
            a.cla()
            a.axis('off')
            a.set_xlim(0, 1)
            a.set_ylim(0, 1)

    # ── ACT 1: CPU vs GPU timeline ─────────────────────────────────────────────
    def act1(f):
        lf = f - A1S
        dur = A1E - A1S
        hide()

        # Title
        fa0 = fp(lf, 0.00, 0.14, dur)
        axT.text(0.5, 0.58, 'CPU → GPU: асинхронный запуск', ha='center', va='center',
                 fontsize=24, fontweight='bold', color=TXT, alpha=fa0)
        axT.text(0.5, 0.15, 'CPU не ждёт GPU — команды в очереди', ha='center', va='center',
                 fontsize=14, color=ACC, alpha=fa0 * 0.9)

        # Timeline layout
        cpu_y = 0.72
        gpu_y = 0.35
        t_start = 0.08
        t_end   = 0.92
        timeline_w = t_end - t_start

        # Draw timeline axes
        ft = fp(lf, 0.08, 0.25, dur)
        if ft > 0:
            # CPU timeline base
            ax_main.annotate('', xy=(t_end + 0.02, cpu_y),
                             xytext=(t_start - 0.02, cpu_y),
                             arrowprops=dict(arrowstyle='->', color=TXT, lw=2.0,
                                             mutation_scale=15),
                             alpha=ft)
            ax_main.text(t_start - 0.04, cpu_y, 'CPU', ha='right', va='center',
                         fontsize=16, fontweight='bold', color=CYAN, alpha=ft)

            # GPU timeline base
            ax_main.annotate('', xy=(t_end + 0.02, gpu_y),
                             xytext=(t_start - 0.02, gpu_y),
                             arrowprops=dict(arrowstyle='->', color=TXT, lw=2.0,
                                             mutation_scale=15),
                             alpha=ft)
            ax_main.text(t_start - 0.04, gpu_y, 'GPU', ha='right', va='center',
                         fontsize=16, fontweight='bold', color=PINK, alpha=ft)

            ax_main.text(t_end + 0.03, cpu_y, 't', ha='left', va='center',
                         fontsize=12, color=TXT, alpha=ft * 0.7)
            ax_main.text(t_end + 0.03, gpu_y, 't', ha='left', va='center',
                         fontsize=12, color=TXT, alpha=ft * 0.7)

        # CPU kernel dispatch blocks (fast, thin)
        kernels = [
            (0.10, 0.06, 'kernel_1\ndispatch', CYAN),
            (0.30, 0.06, 'kernel_2\ndispatch', CYAN),
            (0.52, 0.06, 'kernel_3\ndispatch', CYAN),
        ]
        bar_h = 0.09

        for idx, (kx, kw, klabel, kc) in enumerate(kernels):
            fk = fp(lf, 0.22 + idx * 0.18, 0.40 + idx * 0.18, dur)
            if fk > 0:
                draw_rounded_rect(ax_main, kx, cpu_y - bar_h/2, kw, bar_h,
                                  kc, alpha=fk * 0.85, zorder=3)
                ax_main.text(kx + kw/2, cpu_y, klabel, ha='center', va='center',
                             fontsize=8, color=BG, fontweight='bold', alpha=fk, zorder=4)

        # GPU execution blocks (wider, shifted right — async delay)
        gpu_kernels = [
            (0.22, 0.12, 'kernel_1\nexec', PINK),
            (0.44, 0.12, 'kernel_2\nexec', PINK),
            (0.65, 0.12, 'kernel_3\nexec', PINK),
        ]

        for idx, (kx, kw, klabel, kc) in enumerate(gpu_kernels):
            fk = fp(lf, 0.38 + idx * 0.17, 0.58 + idx * 0.17, dur)
            if fk > 0:
                draw_rounded_rect(ax_main, kx, gpu_y - bar_h/2, kw * fk, bar_h,
                                  kc, alpha=fk * 0.85, zorder=3)
                if fk > 0.7:
                    ax_main.text(kx + kw/2, gpu_y, klabel, ha='center', va='center',
                                 fontsize=8, color=BG, fontweight='bold', alpha=(fk-0.7)/0.3, zorder=4)

        # Arrows showing dispatch → queue
        fa_arr = fp(lf, 0.58, 0.80, dur)
        if fa_arr > 0:
            for cpu_kx, gpu_kx in [(0.13, 0.22), (0.33, 0.44), (0.55, 0.65)]:
                ax_main.annotate('',
                    xy=(gpu_kx + 0.03, gpu_y + bar_h/2),
                    xytext=(cpu_kx + 0.03, cpu_y - bar_h/2),
                    arrowprops=dict(arrowstyle='->', color=ORG, lw=1.5,
                                    connectionstyle='arc3,rad=0.3',
                                    mutation_scale=12),
                    alpha=fa_arr)

        # Async label
        fa_lbl = fp(lf, 0.65, 0.88, dur)
        if fa_lbl > 0:
            ax_main.text(0.5, 0.10, '⚡ GPU выполняет ПОСЛЕ CPU — асинхронно!',
                         ha='center', va='center', fontsize=13,
                         color=YELL, fontweight='bold', alpha=fa_lbl,
                         bbox=dict(boxstyle='round,pad=0.4', facecolor='#1a1a00',
                                   edgecolor=YELL, alpha=fa_lbl * 0.8))

    # ── ACT 2: WRONG WAY ───────────────────────────────────────────────────────
    def act2(f):
        lf = f - A2S
        dur = A2E - A2S
        hide()

        # Title
        fa0 = fp(lf, 0.00, 0.14, dur)
        axT.text(0.5, 0.58, 'Неправильно: time.time()', ha='center', va='center',
                 fontsize=26, fontweight='bold', color=RED, alpha=fa0)
        axT.text(0.5, 0.15, 'Измеряешь CPU dispatch, не GPU вычисления', ha='center', va='center',
                 fontsize=13, color=ACC, alpha=fa0 * 0.9)

        cpu_y = 0.73
        gpu_y = 0.45
        bar_h = 0.09
        t_start = 0.08
        t_end   = 0.92

        # Timelines
        ft = fp(lf, 0.08, 0.22, dur)
        if ft > 0:
            for ty, tcolor, tlabel in [(cpu_y, CYAN, 'CPU'), (gpu_y, PINK, 'GPU')]:
                ax_main.annotate('', xy=(t_end + 0.02, ty),
                                 xytext=(t_start - 0.02, ty),
                                 arrowprops=dict(arrowstyle='->', color=TXT, lw=2.0,
                                                 mutation_scale=15),
                                 alpha=ft)
                ax_main.text(t_start - 0.04, ty, tlabel, ha='right', va='center',
                             fontsize=16, fontweight='bold', color=tcolor, alpha=ft)

        # time.time() START marker
        t_time_start = 0.12
        t_time_end   = 0.36
        ft_s = fp(lf, 0.18, 0.32, dur)
        if ft_s > 0:
            ax_main.axvline(t_time_start, ymin=0.38, ymax=0.85,
                            color=YELL, lw=2.5, ls='--', alpha=ft_s, zorder=5)
            ax_main.text(t_time_start, 0.88, 'time.time()\nstart', ha='center', va='bottom',
                         fontsize=10, color=YELL, fontweight='bold', alpha=ft_s)

        # CPU dispatch (fast)
        f_cpu = fp(lf, 0.26, 0.42, dur)
        if f_cpu > 0:
            draw_rounded_rect(ax_main, t_time_start + 0.01, cpu_y - bar_h/2,
                              0.18 * f_cpu, bar_h, CYAN, alpha=0.85, zorder=3)
            if f_cpu > 0.5:
                ax_main.text(t_time_start + 0.10, cpu_y, 'dispatch\n(~0.1ms)',
                             ha='center', va='center', fontsize=8,
                             color=BG, fontweight='bold', alpha=(f_cpu-0.5)/0.5, zorder=4)

        # time.time() END marker (too early!)
        ft_e = fp(lf, 0.38, 0.52, dur)
        if ft_e > 0:
            ax_main.axvline(t_time_end, ymin=0.38, ymax=0.85,
                            color=YELL, lw=2.5, ls='--', alpha=ft_e, zorder=5)
            ax_main.text(t_time_end, 0.88, 'time.time()\nend', ha='center', va='bottom',
                         fontsize=10, color=YELL, fontweight='bold', alpha=ft_e)

        # "Measured time" bracket on CPU
        ft_br = fp(lf, 0.45, 0.60, dur)
        if ft_br > 0:
            mid_x = (t_time_start + t_time_end) / 2
            ax_main.annotate('', xy=(t_time_end - 0.01, cpu_y + 0.18),
                             xytext=(t_time_start + 0.01, cpu_y + 0.18),
                             arrowprops=dict(arrowstyle='<->', color=YELL, lw=2.0),
                             alpha=ft_br)
            ax_main.text(mid_x, cpu_y + 0.22, '~0.1 ms (CPU dispatch only!)',
                         ha='center', va='bottom', fontsize=11,
                         color=YELL, fontweight='bold', alpha=ft_br)

        # GPU still executing AFTER time.time() end
        f_gpu = fp(lf, 0.50, 0.70, dur)
        if f_gpu > 0:
            gpu_x_start = t_time_end + 0.03
            draw_rounded_rect(ax_main, gpu_x_start, gpu_y - bar_h/2,
                              0.35 * f_gpu, bar_h, PINK, alpha=0.85, zorder=3)
            if f_gpu > 0.5:
                ax_main.text(gpu_x_start + 0.18 * f_gpu, gpu_y, 'actual GPU work\n(~50ms!)',
                             ha='center', va='center', fontsize=8,
                             color=BG, fontweight='bold', alpha=(f_gpu-0.5)/0.5, zorder=4)

        # BIG RED X
        fx = fp(lf, 0.62, 0.80, dur)
        if fx > 0:
            ax_main.text(0.5, 0.10, '✗  Результат: 0.1ms вместо 50ms — ложь!',
                         ha='center', va='center', fontsize=14,
                         color=RED, fontweight='bold', alpha=fx,
                         bbox=dict(boxstyle='round,pad=0.5', facecolor='#2a0000',
                                   edgecolor=RED, alpha=fx * 0.9, lw=2))

        # Red X overlay (crossed out)
        fx2 = fp(lf, 0.72, 0.92, dur)
        if fx2 > 0:
            for x1, y1, x2, y2 in [(0.06, 0.22, 0.94, 0.93),
                                    (0.06, 0.93, 0.94, 0.22)]:
                ax_main.plot([x1, x2], [y1, y2], color=RED, lw=4,
                             alpha=fx2 * 0.35, zorder=10, solid_capstyle='round')

    # ── ACT 3: RIGHT WAY ───────────────────────────────────────────────────────
    def act3(f):
        lf = f - A3S
        dur = A3E - A3S
        hide()

        # Title
        fa0 = fp(lf, 0.00, 0.14, dur)
        axT.text(0.5, 0.58, 'Правильно: torch.cuda.Event', ha='center', va='center',
                 fontsize=24, fontweight='bold', color=GRN, alpha=fa0)
        axT.text(0.5, 0.15, 'Синхронизация на GPU timeline', ha='center', va='center',
                 fontsize=13, color=ACC, alpha=fa0 * 0.9)

        cpu_y = 0.73
        gpu_y = 0.40
        bar_h = 0.09
        t_start = 0.08
        t_end   = 0.92

        # Timelines
        ft = fp(lf, 0.08, 0.22, dur)
        if ft > 0:
            for ty, tcolor, tlabel in [(cpu_y, CYAN, 'CPU'), (gpu_y, PINK, 'GPU')]:
                ax_main.annotate('', xy=(t_end + 0.02, ty),
                                 xytext=(t_start - 0.02, ty),
                                 arrowprops=dict(arrowstyle='->', color=TXT, lw=2.0,
                                                 mutation_scale=15),
                                 alpha=ft)
                ax_main.text(t_start - 0.04, ty, tlabel, ha='right', va='center',
                             fontsize=16, fontweight='bold', color=tcolor, alpha=ft)

        # Code snippet appears
        code_lines = [
            ("start_event = torch.cuda.Event(", CYAN),
            ("    enable_timing=True)", CYAN),
            ("end_event   = torch.cuda.Event(", CYAN),
            ("    enable_timing=True)", CYAN),
            ("", TXT),
            ("start_event.record()", GRN),
            ("model(x)   # GPU kernel", TXT),
            ("end_event.record()", GRN),
            ("torch.cuda.synchronize()  # ← ключ!", YELL),
            ("ms = start_event.elapsed_time(end_event)", ACC),
        ]
        f_code = fp(lf, 0.15, 0.55, dur)
        if f_code > 0:
            n_lines = max(1, int(f_code * len(code_lines)))
            code_bg_h = 0.28
            draw_rounded_rect(ax_main, 0.53, 0.30, 0.44, code_bg_h + 0.04,
                              '#0d1117', alpha=min(1.0, f_code * 1.5), lw=1.5,
                              edgecolor=GRD, zorder=2)
            for li, (line, lc) in enumerate(code_lines[:n_lines]):
                ax_main.text(0.55, 0.57 - li * 0.028, line,
                             fontsize=8.2, color=lc, family='monospace',
                             va='top', alpha=min(1.0, f_code * 2), zorder=3)

        # start_event marker on GPU timeline
        t_event_s = 0.15
        t_event_e = 0.72
        f_es = fp(lf, 0.32, 0.48, dur)
        if f_es > 0:
            ax_main.axvline(t_event_s, ymin=0.28, ymax=0.86,
                            color=GRN, lw=3, ls='--', alpha=f_es, zorder=5)
            ax_main.text(t_event_s, 0.88, 'start_event\n.record()',
                         ha='center', va='bottom', fontsize=9,
                         color=GRN, fontweight='bold', alpha=f_es)

        # GPU kernel execution block
        f_kern = fp(lf, 0.40, 0.58, dur)
        if f_kern > 0:
            draw_rounded_rect(ax_main, t_event_s + 0.02, gpu_y - bar_h/2,
                              (t_event_e - t_event_s - 0.04) * f_kern,
                              bar_h, PINK, alpha=0.85, zorder=3)
            if f_kern > 0.6:
                ax_main.text(t_event_s + (t_event_e - t_event_s) * 0.5,
                             gpu_y, 'GPU computation\n(50+ ms)',
                             ha='center', va='center', fontsize=9,
                             color=BG, fontweight='bold',
                             alpha=(f_kern - 0.6) / 0.4, zorder=4)

        # CPU dispatch (thin)
        f_cpu = fp(lf, 0.38, 0.50, dur)
        if f_cpu > 0:
            draw_rounded_rect(ax_main, t_event_s + 0.01, cpu_y - bar_h/2,
                              0.10, bar_h, CYAN, alpha=0.7, zorder=3)
            ax_main.text(t_event_s + 0.05, cpu_y, 'dispatch',
                         ha='center', va='center', fontsize=8,
                         color=BG, fontweight='bold',
                         alpha=f_cpu, zorder=4)

        # synchronize() marker — CPU waits for GPU
        f_sync = fp(lf, 0.55, 0.70, dur)
        if f_sync > 0:
            ax_main.axvline(t_event_e, ymin=0.28, ymax=0.86,
                            color=YELL, lw=3, ls='--', alpha=f_sync, zorder=5)
            ax_main.text(t_event_e, 0.88, 'end_event\n.record() +\nsynchronize()',
                         ha='center', va='bottom', fontsize=9,
                         color=YELL, fontweight='bold', alpha=f_sync)

            # Sync arrow: CPU waits at GPU end
            ax_main.annotate('',
                xy=(t_event_e, cpu_y - bar_h/2 - 0.02),
                xytext=(t_event_e, gpu_y + bar_h/2 + 0.02),
                arrowprops=dict(arrowstyle='->', color=YELL, lw=2.5,
                                mutation_scale=14,
                                connectionstyle='arc3,rad=0.4'),
                alpha=f_sync)
            ax_main.text(t_event_e + 0.04, (cpu_y + gpu_y) / 2,
                         'CPU\nwaits', ha='left', va='center',
                         fontsize=9, color=YELL, alpha=f_sync)

        # Correct measurement bracket
        f_br = fp(lf, 0.65, 0.82, dur)
        if f_br > 0:
            mid_x = (t_event_s + t_event_e) / 2
            ax_main.annotate('', xy=(t_event_e - 0.01, gpu_y - 0.14),
                             xytext=(t_event_s + 0.01, gpu_y - 0.14),
                             arrowprops=dict(arrowstyle='<->', color=GRN, lw=2.0),
                             alpha=f_br)
            ax_main.text(mid_x, gpu_y - 0.19, '✓ elapsed_time() — реальное GPU время',
                         ha='center', va='top', fontsize=11,
                         color=GRN, fontweight='bold', alpha=f_br,
                         bbox=dict(boxstyle='round,pad=0.3', facecolor='#001a00',
                                   edgecolor=GRN, alpha=f_br * 0.8))

    # ── ACT 4: CHECKLIST ──────────────────────────────────────────────────────
    def act4(f):
        lf = f - A4S
        dur = A4E - A4S
        hide()

        # Title
        fa0 = fp(lf, 0.00, 0.20, dur)
        axT.text(0.5, 0.58, 'Чеклист правильного бенчмарка', ha='center', va='center',
                 fontsize=22, fontweight='bold', color=TXT, alpha=fa0)

        checklist = [
            (GRN,  "✓  Warmup: прогрей GPU (10+ итераций)"),
            (GRN,  "✓  torch.cuda.synchronize() перед .record()"),
            (GRN,  "✓  Используй torch.cuda.Event(enable_timing=True)"),
            (YELL, "✓  Измеряй median, не mean (выбросы!)"),
            (YELL, "✓  Отключи autograd: torch.no_grad()"),
            (YELL, "✓  model.eval() для инференса"),
            (ACC,  "✓  Несколько прогонов → стабильность"),
        ]

        for idx, (color, text) in enumerate(checklist):
            fi = fp(lf, 0.15 + idx * 0.10, 0.30 + idx * 0.10, dur)
            if fi > 0:
                y_pos = 0.83 - idx * 0.11
                # Background pill
                draw_rounded_rect(ax_main, 0.06, y_pos - 0.038, 0.88, 0.076,
                                  color, alpha=fi * 0.12, zorder=2)
                ax_main.plot([0.06, 0.06], [y_pos - 0.028, y_pos + 0.028],
                             color=color, lw=3, alpha=fi, zorder=3,
                             solid_capstyle='round')
                ax_main.text(0.10, y_pos, text,
                             fontsize=13, color=color,
                             va='center', fontweight='bold',
                             alpha=fi, zorder=3)

        # Final code snippet
        f_code = fp(lf, 0.72, 0.95, dur)
        if f_code > 0:
            axB.text(0.5, 0.65,
                     'elapsed_time = start.elapsed_time(end)  # ms, точно!',
                     ha='center', va='center', fontsize=12,
                     color=CYAN, family='monospace',
                     alpha=f_code,
                     bbox=dict(boxstyle='round,pad=0.4', facecolor='#0d1117',
                               edgecolor=CYAN, alpha=f_code * 0.9))

    # ── dispatch ───────────────────────────────────────────────────────────────
    if frame < A1E - BLEND:
        act1(frame)
    elif frame < A2S + BLEND:
        t = (frame - (A1E - BLEND)) / (2 * BLEND)
        if t < 0.5:
            act1(frame)
        else:
            act2(frame)
    elif frame < A2E - BLEND:
        act2(frame)
    elif frame < A3S + BLEND:
        t = (frame - (A2E - BLEND)) / (2 * BLEND)
        if t < 0.5:
            act2(frame)
        else:
            act3(frame)
    elif frame < A3E - BLEND:
        act3(frame)
    elif frame < A4S + BLEND:
        t = (frame - (A3E - BLEND)) / (2 * BLEND)
        if t < 0.5:
            act3(frame)
        else:
            act4(frame)
    else:
        act4(frame)


def main():
    tmpdir = tempfile.mkdtemp(prefix='gpubench_')
    print(f"Rendering {NFRAM} frames ({DUR}s @ {FPS}fps) → {tmpdir}")

    fig  = plt.figure(figsize=(FW, FH), dpi=DPI, facecolor=BG)
    ax_main = fig.add_axes([0.07, 0.13, 0.88, 0.73])
    ax_main.set_facecolor(PAN)
    ax_main.axis('off')
    axT  = fig.add_axes([0.0, 0.87, 1.0, 0.11], facecolor='none')
    axT.set_xlim(0, 1); axT.set_ylim(0, 1); axT.axis('off')
    axB  = fig.add_axes([0.0, 0.00, 1.0, 0.12], facecolor='none')
    axB.set_xlim(0, 1); axB.set_ylim(0, 1); axB.axis('off')
    axes = (ax_main, axT, axB)

    for i in range(NFRAM):
        make_frame(i, fig, axes)
        fig.savefig(os.path.join(tmpdir, f"f{i:04d}.png"),
                    facecolor=BG, dpi=DPI)
        if i % 30 == 0:
            print(f"  {i}/{NFRAM}", flush=True)

    plt.close(fig)
    print("Encoding with ffmpeg ...")

    cmd = ['ffmpeg', '-y',
           '-framerate', str(FPS),
           '-i', os.path.join(tmpdir, 'f%04d.png'),
           '-c:v', 'libx264', '-preset', 'fast', '-crf', '18',
           '-pix_fmt', 'yuv420p', '-movflags', '+faststart',
           OUTPUT]
    r = subprocess.run(cmd, capture_output=True, text=True)
    if r.returncode != 0:
        print("ffmpeg error:\n", r.stderr[-2000:])
        sys.exit(1)

    import shutil; shutil.rmtree(tmpdir)
    size_mb = os.path.getsize(OUTPUT) / 1e6
    print(f"Done! {OUTPUT} ({size_mb:.1f} MB)")

    # Verify with ffprobe
    probe = subprocess.run(
        ['ffprobe', '-v', 'quiet', '-print_format', 'json',
         '-show_streams', OUTPUT],
        capture_output=True, text=True)
    print("ffprobe:", probe.stdout[:500] if probe.stdout else probe.stderr[:300])


if __name__ == '__main__':
    main()
