"""
GNN Message Passing animation for @fminxyz Series 17 Post 1
4 phases, 25 seconds, 1080x1080, dark background
"""

import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.patheffects as pe
from matplotlib.animation import FuncAnimation, FFMpegWriter
from matplotlib.patches import FancyArrowPatch
import matplotlib.colors as mcolors

# Style
BG = '#0a0a1a'
BLUE = '#4a9eff'
GREEN = '#00ff88'
ORANGE = '#ff8c42'
PURPLE = '#c77dff'
WHITE = '#ffffff'
GRAY = '#888888'

FPS = 25
DURATION = 25
FRAMES = FPS * DURATION

fig, ax = plt.subplots(1, 1, figsize=(10.8, 10.8), dpi=100)
fig.patch.set_facecolor(BG)

def clear_ax():
    ax.clear()
    ax.set_facecolor(BG)
    ax.set_xlim(0, 10)
    ax.set_ylim(0, 10)
    ax.set_aspect('equal')
    ax.axis('off')

# Caffeine-like molecule graph (simplified 6-atom version)
atom_pos = np.array([
    [5.0, 7.5],   # 0 N
    [3.5, 6.5],   # 1 C
    [3.8, 4.8],   # 2 C
    [5.5, 4.2],   # 3 N
    [6.8, 5.5],   # 4 C
    [6.2, 7.0],   # 5 C
    [2.5, 5.8],   # 6 O (side)
    [5.8, 3.0],   # 7 O (side)
])

atom_types = ['N', 'C', 'C', 'N', 'C', 'C', 'O', 'O']
atom_colors = {'N': BLUE, 'C': GREEN, 'O': ORANGE}

edges = [(0,1),(1,2),(2,3),(3,4),(4,5),(5,0),(2,6),(3,7),(0,5)]

def draw_graph(alpha=1.0, highlight_node=None, highlight_edges=None, labels=True):
    # Draw edges
    for (u, v) in edges:
        pu, pv = atom_pos[u], atom_pos[v]
        is_hl = highlight_edges is not None and ((u,v) in highlight_edges or (v,u) in highlight_edges)
        color = ORANGE if is_hl else '#444466'
        lw = 3 if is_hl else 1.5
        ax.plot([pu[0], pv[0]], [pu[1], pv[1]], color=color, lw=lw, alpha=alpha, zorder=1)

    # Draw nodes
    for i, (pos, atype) in enumerate(zip(atom_pos, atom_types)):
        size = 400
        color = atom_colors[atype]
        edge_c = WHITE if i == highlight_node else color
        ec_lw = 3 if i == highlight_node else 1
        ax.scatter(*pos, s=size, color=color, zorder=3, alpha=alpha, edgecolors=edge_c, linewidths=ec_lw)
        if labels:
            ax.text(pos[0], pos[1], atype, ha='center', va='center',
                   fontsize=11, fontweight='bold', color=BG, zorder=4)

def draw_message_arrows(center, neighbors, alpha=1.0):
    for nb in neighbors:
        start = atom_pos[nb]
        end = atom_pos[center]
        dx, dy = end[0]-start[0], end[1]-start[1]
        ax.annotate('', xy=(end[0]-dx*0.12, end[1]-dy*0.12),
                   xytext=(start[0]+dx*0.12, start[1]+dy*0.12),
                   arrowprops=dict(arrowstyle='->', color=ORANGE, lw=2.5),
                   zorder=5, alpha=alpha)

def animate(frame):
    clear_ax()
    t = frame / FRAMES

    phase_bounds = [0.0, 0.25, 0.50, 0.75, 1.0]

    if t < phase_bounds[1]:
        # Phase 1: Show molecule graph
        p = t / phase_bounds[1]

        ax.text(5, 9.5, 'Граф молекулы кофеина', ha='center', va='top',
               fontsize=20, fontweight='bold', color=WHITE)
        ax.text(5, 9.0, 'Атомы = вершины, связи = рёбра', ha='center', va='top',
               fontsize=14, color=GRAY)

        draw_graph(alpha=min(1.0, p*2))

        if p > 0.5:
            # Show legend
            legend_items = [
                (GREEN, 'C — углерод'), (BLUE, 'N — азот'), (ORANGE, 'O — кислород')
            ]
            for i, (c, label) in enumerate(legend_items):
                ax.scatter(1.5, 3.5 - i*0.7, s=200, color=c, zorder=5)
                ax.text(2.0, 3.5 - i*0.7, label, va='center', fontsize=12, color=WHITE)

        # Formula
        ax.text(5, 1.0, 'C₈H₁₀N₄O₂', ha='center', fontsize=16, color=BLUE)

    elif t < phase_bounds[2]:
        # Phase 2: Message Passing step 1 — center node collects from neighbors
        p = (t - phase_bounds[1]) / (phase_bounds[2] - phase_bounds[1])

        ax.text(5, 9.5, 'Message Passing: шаг 1', ha='center', va='top',
               fontsize=20, fontweight='bold', color=WHITE)
        ax.text(5, 9.0, 'Вершина 4 (C) собирает сообщения от соседей', ha='center', va='top',
               fontsize=13, color=GRAY)

        center_node = 4
        neighbors = [3, 5]

        draw_graph(highlight_node=center_node, highlight_edges=[(3,4),(4,5)])

        if p > 0.3:
            draw_message_arrows(center_node, neighbors, alpha=min(1.0, (p-0.3)*3))

        # Code
        code_y = 2.8
        ax.text(5, code_y, 'mᵥ = AGG({hᵤ : u ∈ N(v)})', ha='center', fontsize=14,
               color=GREEN, family='monospace',
               bbox=dict(boxstyle='round', facecolor='#1a1a2e', edgecolor=GREEN, alpha=0.8))
        ax.text(5, 2.1, 'hᵥ\' = UPDATE(hᵥ, mᵥ)', ha='center', fontsize=14,
               color=ORANGE, family='monospace',
               bbox=dict(boxstyle='round', facecolor='#1a1a2e', edgecolor=ORANGE, alpha=0.8))

    elif t < phase_bounds[3]:
        # Phase 3: Receptive field expands with more steps
        p = (t - phase_bounds[2]) / (phase_bounds[3] - phase_bounds[2])

        ax.text(5, 9.5, 'Рецептивное поле расширяется', ha='center', va='top',
               fontsize=20, fontweight='bold', color=WHITE)
        ax.text(5, 9.0, 'После K итераций — окрестность радиуса K', ha='center', va='top',
               fontsize=13, color=GRAY)

        draw_graph()

        # Show expanding rings
        center = atom_pos[4]
        steps = min(3, int(p * 4))
        colors_ring = [ORANGE, BLUE, GREEN]
        radii = [0.8, 1.6, 2.5]
        labels_ring = ['K=1', 'K=2', 'K=3']
        for i in range(steps):
            circle = plt.Circle(center, radii[i], fill=False,
                               color=colors_ring[i], lw=2, linestyle='--', alpha=0.6, zorder=2)
            ax.add_patch(circle)
            ax.text(center[0] + radii[i] * 0.7, center[1] + radii[i] * 0.7, labels_ring[i],
                   fontsize=11, color=colors_ring[i], fontweight='bold')

        # Step count
        ax.text(5, 1.5, f'K = {steps} итерации', ha='center', fontsize=16,
               color=ORANGE, fontweight='bold')

    else:
        # Phase 4: Conv2D vs GNN comparison
        p = (t - phase_bounds[3]) / (phase_bounds[3] - phase_bounds[3] + (1.0 - phase_bounds[3]))

        ax.text(5, 9.5, 'Conv2D — частный случай GNN', ha='center', va='top',
               fontsize=20, fontweight='bold', color=WHITE)

        # Left: regular grid (Conv2D)
        ax.text(2.5, 8.5, 'Conv2D', ha='center', fontsize=15, color=BLUE, fontweight='bold')
        ax.text(2.5, 8.0, 'Регулярная сетка', ha='center', fontsize=11, color=GRAY)
        grid_size = 4
        for i in range(grid_size):
            for j in range(grid_size):
                x, y = 1.0 + j*0.8, 3.5 + i*0.8
                color = ORANGE if (i == 2 and j == 2) else (BLUE if abs(i-2)+abs(j-2) == 1 else '#334')
                ax.scatter(x, y, s=120, color=color, zorder=3)
                if abs(i-2)+abs(j-2) <= 1:
                    ax.scatter(x, y, s=200, color=color, zorder=3, alpha=0.5)

        # Right: irregular graph (GNN)
        ax.text(7.5, 8.5, 'GNN', ha='center', fontsize=15, color=GREEN, fontweight='bold')
        ax.text(7.5, 8.0, 'Произвольный граф', ha='center', fontsize=11, color=GRAY)

        irr_pos = [(7.5, 7.0), (6.2, 5.8), (8.8, 5.5), (7.0, 4.5), (8.5, 4.0), (6.0, 3.5)]
        irr_edges = [(0,1),(0,2),(1,2),(1,3),(2,4),(3,5),(2,3)]
        for e in irr_edges:
            ax.plot([irr_pos[e[0]][0], irr_pos[e[1]][0]],
                   [irr_pos[e[0]][1], irr_pos[e[1]][1]],
                   color='#334', lw=1.5)
        for i, pos in enumerate(irr_pos):
            color = ORANGE if i == 0 else (GREEN if i in [1,2] else '#334')
            ax.scatter(*pos, s=150, color=color if color != '#334' else '#446',
                      zorder=3, edgecolors=color, linewidths=1.5)

        # Divider
        ax.axvline(5, color='#334', lw=1, linestyle=':')

        # Key insight
        ax.text(5, 2.5, 'Одна и та же идея:', ha='center', fontsize=13, color=GRAY)
        ax.text(5, 2.0, 'агрегировать соседей → обновить себя', ha='center',
               fontsize=14, color=WHITE, fontweight='bold')
        ax.text(5, 1.4, 'Conv2D = GNN на регулярной сетке', ha='center',
               fontsize=13, color=BLUE)

    plt.tight_layout(pad=0.3)

anim = FuncAnimation(fig, animate, frames=FRAMES, interval=1000/FPS, blit=False)

writer = FFMpegWriter(fps=FPS, metadata={'title': 'GNN Message Passing'},
                     bitrate=1800, extra_args=['-vcodec', 'libx264', '-pix_fmt', 'yuv420p'])
output_path = '/root/Strategy/content/generated/gnn_message_passing_animation.mp4'
anim.save(output_path, writer=writer)
print(f"Saved: {output_path}")

import os
size = os.path.getsize(output_path) // 1024
print(f"Size: {size}KB")
plt.close()
