"""
Gradient Descent на разных поверхностях
4 loss landscapes side-by-side: convex, Rosenbrock, Rastrigin, saddle point
1080x1080, 25 seconds, 30fps, dark theme
"""
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation, FFMpegWriter
from matplotlib import cm
from matplotlib.colors import LinearSegmentedColormap

# === Config ===
FPS = 30
DURATION = 25
N_FRAMES = FPS * DURATION
W, H = 1080, 1080
DPI = 120

# Dark theme
BG_COLOR = '#1a1a2e'
TEXT_COLOR = '#e0e0e0'
ACCENT_COLORS = ['#00d2ff', '#ff6b6b', '#ffd93d', '#6bcb77']

# Custom colormap (dark-themed)
colors_cmap = ['#1a1a2e', '#16213e', '#0f3460', '#533483', '#e94560']
custom_cmap = LinearSegmentedColormap.from_list('dark_heat', colors_cmap, N=256)

# === Loss functions ===
def convex(x, y):
    return x**2 + y**2

def rosenbrock(x, y):
    a, b = 1, 100
    return (a - x)**2 + b * (y - x**2)**2

def rastrigin(x, y):
    A = 10
    return A * 2 + (x**2 - A * np.cos(2 * np.pi * x)) + (y**2 - A * np.cos(2 * np.pi * y))

def saddle(x, y):
    return x**2 - y**2

# === Gradients ===
def grad_convex(x, y):
    return 2*x, 2*y

def grad_rosenbrock(x, y):
    a, b = 1, 100
    dx = -2*(a - x) + b * 2 * (y - x**2) * (-2*x)
    dy = b * 2 * (y - x**2)
    return dx, dy

def grad_rastrigin(x, y):
    A = 10
    dx = 2*x + A * 2 * np.pi * np.sin(2 * np.pi * x)
    dy = 2*y + A * 2 * np.pi * np.sin(2 * np.pi * y)
    return dx, dy

def grad_saddle(x, y):
    return 2*x, -2*y

# === Pre-compute trajectories ===
def run_gd(grad_fn, x0, y0, lr, n_steps, clip_val=5.0):
    traj = [(x0, y0)]
    x, y = x0, y0
    for _ in range(n_steps):
        gx, gy = grad_fn(x, y)
        # clip gradient
        norm = np.sqrt(gx**2 + gy**2)
        if norm > clip_val:
            gx, gy = gx * clip_val / norm, gy * clip_val / norm
        x -= lr * gx
        y -= lr * gy
        traj.append((x, y))
    return np.array(traj)

N_STEPS = N_FRAMES

configs = [
    {
        'name': 'Convex (квадрат.)',
        'func': convex, 'grad': grad_convex,
        'x0': 1.8, 'y0': 1.8, 'lr': 0.02,
        'xlim': (-2.5, 2.5), 'ylim': (-2.5, 2.5),
        'color': ACCENT_COLORS[0],
        'levels': 20,
    },
    {
        'name': 'Rosenbrock',
        'func': rosenbrock, 'grad': grad_rosenbrock,
        'x0': -1.5, 'y0': 2.0, 'lr': 0.0005,
        'xlim': (-2.5, 2.5), 'ylim': (-1.5, 3.5),
        'color': ACCENT_COLORS[1],
        'levels': np.logspace(0, 4, 25),
    },
    {
        'name': 'Rastrigin',
        'func': rastrigin, 'grad': grad_rastrigin,
        'x0': 3.5, 'y0': 3.5, 'lr': 0.002,
        'xlim': (-5, 5), 'ylim': (-5, 5),
        'color': ACCENT_COLORS[2],
        'levels': 25,
    },
    {
        'name': 'Седловая точка',
        'func': saddle, 'grad': grad_saddle,
        'x0': 1.5, 'y0': 0.3, 'lr': 0.01,
        'xlim': (-2.5, 2.5), 'ylim': (-2.5, 2.5),
        'color': ACCENT_COLORS[3],
        'levels': 20,
    },
]

# Pre-compute
trajectories = []
for cfg in configs:
    traj = run_gd(cfg['grad'], cfg['x0'], cfg['y0'], cfg['lr'], N_STEPS)
    trajectories.append(traj)

# === Create figure ===
fig, axes = plt.subplots(2, 2, figsize=(W/DPI, H/DPI), dpi=DPI)
fig.patch.set_facecolor(BG_COLOR)
fig.suptitle('Gradient Descent на разных ландшафтах',
             fontsize=18, color=TEXT_COLOR, fontweight='bold', y=0.97,
             fontfamily='sans-serif')

contour_sets = []
trail_lines = []
point_markers = []
loss_texts = []
step_text = fig.text(0.5, 0.015, '', ha='center', va='bottom',
                     fontsize=13, color='#888888', fontfamily='monospace')

for idx, (ax, cfg) in enumerate(zip(axes.flat, configs)):
    ax.set_facecolor(BG_COLOR)
    ax.set_xlim(cfg['xlim'])
    ax.set_ylim(cfg['ylim'])
    ax.set_aspect('equal')
    ax.set_title(cfg['name'], fontsize=13, color=cfg['color'], fontweight='bold', pad=6)
    ax.tick_params(colors='#555555', labelsize=7)
    for spine in ax.spines.values():
        spine.set_color('#333355')

    # Contour
    xx = np.linspace(cfg['xlim'][0], cfg['xlim'][1], 300)
    yy = np.linspace(cfg['ylim'][0], cfg['ylim'][1], 300)
    X, Y = np.meshgrid(xx, yy)
    Z = cfg['func'](X, Y)

    if isinstance(cfg['levels'], np.ndarray):
        cs = ax.contourf(X, Y, Z, levels=cfg['levels'], cmap=custom_cmap, alpha=0.85)
        ax.contour(X, Y, Z, levels=cfg['levels'], colors='#333355', linewidths=0.3, alpha=0.5)
    else:
        cs = ax.contourf(X, Y, Z, levels=cfg['levels'], cmap=custom_cmap, alpha=0.85)
        ax.contour(X, Y, Z, levels=cfg['levels'], colors='#333355', linewidths=0.3, alpha=0.5)
    contour_sets.append(cs)

    # Trail line
    line, = ax.plot([], [], color=cfg['color'], linewidth=1.5, alpha=0.7)
    trail_lines.append(line)

    # Current point
    point, = ax.plot([], [], 'o', color=cfg['color'], markersize=8,
                     markeredgecolor='white', markeredgewidth=1.5, zorder=10)
    point_markers.append(point)

    # Loss text
    txt = ax.text(0.05, 0.92, '', transform=ax.transAxes, fontsize=9,
                  color=cfg['color'], fontfamily='monospace', fontweight='bold',
                  bbox=dict(boxstyle='round,pad=0.3', facecolor=BG_COLOR,
                           edgecolor=cfg['color'], alpha=0.8))
    loss_texts.append(txt)

plt.tight_layout(rect=[0.01, 0.04, 0.99, 0.94])

def animate(frame):
    for idx, cfg in enumerate(configs):
        traj = trajectories[idx]
        # Show trajectory up to current frame
        show_len = min(frame + 1, len(traj))
        trail_lines[idx].set_data(traj[:show_len, 0], traj[:show_len, 1])

        cx, cy = traj[min(frame, len(traj)-1)]
        point_markers[idx].set_data([cx], [cy])

        loss_val = cfg['func'](cx, cy)
        loss_texts[idx].set_text(f'L = {loss_val:.2f}')

    step_text.set_text(f'step {frame}/{N_FRAMES}')
    return trail_lines + point_markers + loss_texts + [step_text]

anim = FuncAnimation(fig, animate, frames=N_FRAMES, interval=1000/FPS, blit=True)

outpath = '/root/Strategy/content/generated/gradient_surfaces_animation.mp4'
writer = FFMpegWriter(fps=FPS, bitrate=3000,
                      extra_args=['-vcodec', 'libx264', '-pix_fmt', 'yuv420p'])
anim.save(outpath, writer=writer)
plt.close()
print(f"Saved: {outpath}")
