"""
CFG (Classifier-Free Guidance) Animation: vector arithmetic in 2D score space
@fminxyz Series 2, Post 5 — Финал серии
1080x1080 px, 2 fps, 25 сек (~50 frames)
"""

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.patches import FancyArrowPatch

OUTPUT = "/root/Strategy/content/drafts/cfg_vector_field_animation.mp4"
FPS = 2
N_FRAMES = 50

rng = np.random.default_rng(42)

# ── Color scheme (dark bg, same series palette) ───────────────────────────────
BG_COLOR = '#0a0a14'
COLOR_UNCOND = '#666688'     # grey — unconditional score
COLOR_COND   = '#4fc3f7'     # blue — conditional score (cat)
COLOR_CFG    = '#ff4466'     # red — CFG boosted
COLOR_TRAJ   = '#ffdd44'     # yellow — sample trajectory

# ── Gaussian mixture: 3 modes in 2D ──────────────────────────────────────────
# Mode 0: "cat"       — upper left
# Mode 1: "dog"       — upper right
# Mode 2: "landscape" — bottom center

MODES = {
    'cat':       {'mean': np.array([-1.8,  1.4]), 'std': 0.55, 'color': '#4fc3f7', 'label': 'cat'},
    'dog':       {'mean': np.array([ 1.8,  1.4]), 'std': 0.55, 'color': '#f06292', 'label': 'dog'},
    'landscape': {'mean': np.array([ 0.0, -1.6]), 'std': 0.65, 'color': '#a5d6a7', 'label': 'landscape'},
}
MODE_LIST = ['cat', 'dog', 'landscape']
MODE_WEIGHTS = np.array([1/3, 1/3, 1/3])

# Target class for conditioning: cat
TARGET_CLASS = 'cat'
TARGET_MEAN = MODES[TARGET_CLASS]['mean']

# ── Score functions ───────────────────────────────────────────────────────────

def gaussian_score(x, mean, std):
    """Score of a single Gaussian: -(x - mean) / std^2"""
    return -(x - mean) / (std ** 2)

def mixture_score_uncond(x, weights=None):
    """Score of full Gaussian mixture (unconditional)."""
    if weights is None:
        weights = MODE_WEIGHTS
    # Compute responsibilities
    log_probs = []
    for name in MODE_LIST:
        m = MODES[name]
        diff = x - m['mean']
        log_p = -0.5 * np.sum(diff**2) / m['std']**2
        log_probs.append(log_p + np.log(weights[MODE_LIST.index(name)]))
    log_probs = np.array(log_probs)
    log_probs -= log_probs.max()
    responsibilities = np.exp(log_probs)
    responsibilities /= responsibilities.sum() + 1e-12

    score = np.zeros(2)
    for i, name in enumerate(MODE_LIST):
        m = MODES[name]
        score += responsibilities[i] * gaussian_score(x, m['mean'], m['std'])
    return score

def mixture_score_cond(x):
    """Score conditioned on 'cat': upweights cat mode strongly."""
    # p(x|cat) ∝ p(x) * p(cat|x), approximate as sharp focus on cat mode
    weights_cond = np.array([0.88, 0.08, 0.04])  # strong cat weighting
    return mixture_score_uncond(x, weights=weights_cond)

def cfg_score(x, w):
    """ε_cfg = ε_uncond + w * (ε_cond - ε_uncond)"""
    s_u = mixture_score_uncond(x)
    s_c = mixture_score_cond(x)
    return s_u + w * (s_c - s_u)

# ── Grid for vector fields ────────────────────────────────────────────────────
XLIM = (-3.2, 3.2)
YLIM = (-3.0, 3.0)
NX, NY = 13, 13
xg = np.linspace(XLIM[0], XLIM[1], NX)
yg = np.linspace(YLIM[0], YLIM[1], NY)
Xg, Yg = np.meshgrid(xg, yg)
grid_pts = np.stack([Xg.ravel(), Yg.ravel()], axis=1)

def compute_field(score_fn):
    vecs = np.array([score_fn(p) for p in grid_pts])
    Vx = vecs[:, 0].reshape(Xg.shape)
    Vy = vecs[:, 1].reshape(Yg.shape)
    return Vx, Vy

def normalize_field(Vx, Vy, scale=0.35):
    mag = np.sqrt(Vx**2 + Vy**2 + 1e-12)
    return Vx / mag * scale, Vy / mag * scale

# Precompute fields
Vx_u, Vy_u = compute_field(mixture_score_uncond)
Vx_c, Vy_c = compute_field(mixture_score_cond)

W_VALUES = [0.0, 1.0, 3.0, 7.0, 15.0, 30.0]
W_LABELS = ['w=0  (unconditional)', 'w=1  (just conditional)', 'w=3  (light boost)',
            'w=7  (Stable Diffusion default)', 'w=15  (strong)', 'w=30  (oversaturated)']

# ── Sample trajectory: follow CFG field from noise ───────────────────────────
# Compute trajectory for each w value
N_TRAJ_STEPS = 60
TRAJ_START = np.array([-0.3, -0.1])   # near center (noise-like)

def compute_trajectory(w, n_steps=N_TRAJ_STEPS, step_size=0.06):
    pos = TRAJ_START.copy()
    path = [pos.copy()]
    for _ in range(n_steps):
        grad = cfg_score(pos, w)
        mag = np.linalg.norm(grad) + 1e-8
        pos = pos + step_size * grad / mag
        # Clamp to plot area
        pos[0] = np.clip(pos[0], XLIM[0]+0.1, XLIM[1]-0.1)
        pos[1] = np.clip(pos[1], YLIM[0]+0.1, YLIM[1]-0.1)
        path.append(pos.copy())
    return np.array(path)

trajectories = {w: compute_trajectory(w) for w in W_VALUES}

# ── Frame schedule ────────────────────────────────────────────────────────────
# 50 frames total, 6 w values: each gets ~8 frames
# Frame layout:
#   0-1:   title / intro
#   2-9:   w=0  (8 frames)
#  10-17:  w=1  (8 frames)
#  18-25:  w=3  (8 frames)
#  26-33:  w=7  (8 frames, "sweet spot" highlight)
#  34-41:  w=15 (8 frames)
#  42-49:  w=30 (8 frames, oversaturation)

PHASE_STARTS = [2, 10, 18, 26, 34, 42]
PHASE_ENDS   = [9, 17, 25, 33, 41, 49]

def get_phase(frame):
    """Returns (phase_idx, local_frame 0..7) or (-1, 0) for intro."""
    if frame < 2:
        return -1, frame
    for i, (s, e) in enumerate(zip(PHASE_STARTS, PHASE_ENDS)):
        if s <= frame <= e:
            return i, frame - s
    return 5, frame - PHASE_STARTS[5]

# ── Figure ────────────────────────────────────────────────────────────────────
DPI = 108
fig, ax = plt.subplots(figsize=(10, 10), dpi=DPI, facecolor=BG_COLOR)

def draw_mode_blobs(ax, alpha=0.25):
    """Draw soft Gaussian blobs for each concept mode."""
    for name in MODE_LIST:
        m = MODES[name]
        theta = np.linspace(0, 2*np.pi, 60)
        for r_scale, a in [(1.0, alpha), (0.6, alpha*1.4), (0.3, alpha*1.8)]:
            rx = m['mean'][0] + m['std'] * r_scale * np.cos(theta)
            ry = m['mean'][1] + m['std'] * r_scale * np.sin(theta)
            ax.fill(rx, ry, color=m['color'], alpha=a, zorder=1)
        # Mode label
        ax.text(m['mean'][0], m['mean'][1] + m['std'] + 0.22,
                m['label'], ha='center', fontsize=13,
                color=m['color'], fontfamily='monospace', alpha=0.85, zorder=6)

def draw_frame(i):
    ax.clear()
    ax.set_facecolor(BG_COLOR)
    ax.set_xlim(XLIM)
    ax.set_ylim(YLIM)
    ax.set_aspect('equal')
    ax.axis('off')

    phase, local = get_phase(i)
    fade_in = min(1.0, (i + 1) / 4.0)

    # ── Intro frames ──────────────────────────────────────────────────────────
    if phase == -1:
        draw_mode_blobs(ax, alpha=0.18)
        ax.text(0, 2.6, 'Classifier-Free Guidance',
                ha='center', fontsize=19, fontweight='bold', color='white',
                fontfamily='monospace', alpha=fade_in)
        ax.text(0, 2.15, 'ε_cfg = ε_uncond + w·(ε_cond − ε_uncond)',
                ha='center', fontsize=13, color='#aaaaee',
                fontfamily='monospace', alpha=fade_in)
        ax.text(0, -2.6, 'три концепции в пространстве шума',
                ha='center', fontsize=12, color='#556677',
                fontfamily='monospace', alpha=fade_in)
        return

    # ── Phase frames ──────────────────────────────────────────────────────────
    w_val = W_VALUES[phase]
    w_label = W_LABELS[phase]
    local_frac = local / 7.0

    # Fade in at start of each phase
    phase_alpha = min(1.0, (local + 1) / 3.0)

    # Draw concept blobs
    blob_alpha = 0.18
    if phase >= 3:  # w=7+: cat blob brighter
        draw_mode_blobs(ax, alpha=0.14)
        # Extra glow on cat for high w
        cat_m = MODES['cat']
        cat_glow = min(0.5, 0.18 + (w_val - 7) * 0.008)
        theta = np.linspace(0, 2*np.pi, 60)
        ax.fill(cat_m['mean'][0] + cat_m['std']*1.2*np.cos(theta),
                cat_m['mean'][1] + cat_m['std']*1.2*np.sin(theta),
                color=cat_m['color'], alpha=cat_glow, zorder=1)
    else:
        draw_mode_blobs(ax, alpha=blob_alpha)

    # ── Unconditional field (always shown, grey, faint) ───────────────────────
    Vx_un, Vy_un = normalize_field(Vx_u, Vy_u, scale=0.28)
    uncond_alpha = 0.30 if phase > 0 else 0.55 * phase_alpha
    ax.quiver(Xg, Yg, Vx_un, Vy_un,
              color=COLOR_UNCOND, alpha=uncond_alpha,
              scale=7.5, headwidth=3, headlength=4,
              width=0.0025, zorder=3)

    # ── Conditional field (shown from phase 1+, blue) ─────────────────────────
    if phase >= 1:
        cond_alpha = min(0.55, phase_alpha * 0.55) if phase == 1 else 0.35
        Vx_cn, Vy_cn = normalize_field(Vx_c, Vy_c, scale=0.28)
        ax.quiver(Xg, Yg, Vx_cn, Vy_cn,
                  color=COLOR_COND, alpha=cond_alpha,
                  scale=7.5, headwidth=3, headlength=4,
                  width=0.0025, zorder=3)

    # ── CFG field (main, red) ─────────────────────────────────────────────────
    if phase >= 1:
        Vx_cfg_raw, Vy_cfg_raw = compute_field(lambda x: cfg_score(x, w_val))
        Vx_cfgn, Vy_cfgn = normalize_field(Vx_cfg_raw, Vy_cfg_raw, scale=0.30)
        cfg_alpha = min(0.85, phase_alpha * 0.85)
        lw = 0.0028 + phase * 0.0003  # slightly thicker for higher w
        ax.quiver(Xg, Yg, Vx_cfgn, Vy_cfgn,
                  color=COLOR_CFG, alpha=cfg_alpha,
                  scale=7.0, headwidth=4, headlength=5,
                  width=lw, zorder=4)

    # ── Sample trajectory ─────────────────────────────────────────────────────
    traj = trajectories[w_val]
    # Show trajectory progress over local frames
    traj_steps = int(local_frac * len(traj))
    traj_steps = max(2, traj_steps)
    traj_show = traj[:traj_steps]

    if len(traj_show) > 1:
        ax.plot(traj_show[:, 0], traj_show[:, 1],
                '-', color=COLOR_TRAJ, alpha=0.75, linewidth=2.0, zorder=7)

    # Current position dot
    cur = traj_show[-1]
    ax.plot(cur[0], cur[1], 'o', color=COLOR_TRAJ,
            markersize=10, alpha=0.95, zorder=8,
            markeredgecolor='white', markeredgewidth=1.0)

    # Start marker
    ax.plot(TRAJ_START[0], TRAJ_START[1], 's',
            color='white', markersize=7, alpha=0.6, zorder=7)

    # ── w value annotation (big) ──────────────────────────────────────────────
    # Color gradient: grey→blue→orange→red for increasing w
    w_colors = ['#888899', '#4fc3f7', '#ffa040', '#ff4466', '#ff1133', '#cc0022']
    wc = w_colors[phase]

    ax.text(0, 2.62, w_label,
            ha='center', fontsize=15, fontweight='bold',
            color=wc, fontfamily='monospace', alpha=phase_alpha, zorder=9)

    # ── CFG formula line ──────────────────────────────────────────────────────
    ax.text(0, 2.28, 'ε_cfg = ε_uncond + w·(ε_cond − ε_uncond)',
            ha='center', fontsize=11, color='#8888aa',
            fontfamily='monospace', alpha=0.7, zorder=9)

    # ── Status annotation ─────────────────────────────────────────────────────
    status_msgs = {
        0: 'только общее направление к данным',
        1: 'guidance = conditional = нет усиления',
        2: 'небольшое усиление "кошачести"',
        3: 'типичный SD: баланс качество/разнообразие ✓',
        4: 'сильное усиление — теряем разнообразие',
        5: 'артефакты: "пережаренное" изображение ⚠️',
    }
    status_colors = ['#888899', '#4fc3f7', '#aaddff', '#44ff88', '#ffaa44', '#ff4444']
    ax.text(0, -2.58, status_msgs[phase],
            ha='center', fontsize=12,
            color=status_colors[phase], fontfamily='monospace',
            alpha=phase_alpha, zorder=9)

    # ── Legend (compact, bottom right) ───────────────────────────────────────
    leg_x, leg_y = 2.4, -1.6
    leg_entries = [
        (COLOR_UNCOND, 'ε_uncond (grey)'),
        (COLOR_COND,   'ε_cond  (blue)'),
        (COLOR_CFG,    'ε_cfg   (red) '),
        (COLOR_TRAJ,   'trajectory    '),
    ]
    for k, (lc, lt) in enumerate(leg_entries):
        ax.text(leg_x, leg_y - k * 0.28, lt,
                ha='left', fontsize=9, color=lc,
                fontfamily='monospace', alpha=0.7, zorder=9)

    # ── Title ─────────────────────────────────────────────────────────────────
    ax.text(0, -2.85, 'CFG: экстраполяция в пространстве score',
            ha='center', fontsize=11, color='#445566',
            fontfamily='monospace', alpha=0.7, zorder=9)

    # ── Frame indicator ───────────────────────────────────────────────────────
    ax.text(XLIM[1]-0.1, YLIM[0]+0.1, f'f={i:02d}',
            ha='right', fontsize=9, color='#334455',
            fontfamily='monospace', zorder=9)

    # ── Phase progress dots ───────────────────────────────────────────────────
    dot_y = YLIM[0] + 0.35
    dot_xs = np.linspace(-1.5, 1.5, len(W_VALUES))
    for di, dx in enumerate(dot_xs):
        dc = '#ff4466' if di == phase else ('#445566' if di < phase else '#223344')
        ax.plot(dx, dot_y, 'o', color=dc, markersize=6, alpha=0.8, zorder=9)


anim = animation.FuncAnimation(fig, draw_frame, frames=N_FRAMES,
                                interval=1000 // FPS)
anim.save(OUTPUT, writer='ffmpeg', fps=FPS, dpi=DPI,
          extra_args=['-vcodec', 'libx264', '-pix_fmt', 'yuv420p',
                      '-crf', '22', '-preset', 'fast'])
plt.close()
print(f"Saved: {OUTPUT}")
