import cv2
import numpy as np
import matplotlib.pyplot as plt
import os

# ------------------------------------------------------------
# True roots: 10th roots of unity
# ------------------------------------------------------------
n_roots = 10
true_roots = np.exp(2j * np.pi * np.arange(n_roots) / n_roots)

# ------------------------------------------------------------
# Polynomial and its derivative (for gradient)
# ------------------------------------------------------------
coeffs = np.poly(true_roots)  # monic polynomial coefficients
deriv_coeffs = coeffs[:-1] * np.arange(len(coeffs) - 1, 0, -1)

def poly_val(z):
    return np.polyval(coeffs, z)

def poly_deriv(z):
    return np.polyval(deriv_coeffs, z)

# ------------------------------------------------------------
# Master feature extraction from video frame
# ------------------------------------------------------------
def extract_master_feature(frame):
    """Returns a complex number: (brightness_left - brightness_right) + i*(mean brightness)"""
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    h, w = gray.shape
    left = gray[:, :w//2].mean()
    right = gray[:, w//2:].mean()
    full_mean = gray.mean()
    # Complex master signal: real = left-right, imag = full_mean
    return complex(left - right, full_mean)

# ------------------------------------------------------------
# Video‑driven root solver
# ------------------------------------------------------------
def solve_roots_with_video(video_path, alpha=0.01, num_iterations=500):
    cap = cv2.VideoCapture(video_path)
    if not cap.isOpened():
        raise RuntimeError(f"Could not open video: {video_path}")

    # Initialize root estimates randomly (on unit circle)
    z = np.exp(2j * np.pi * np.random.rand(n_roots)).astype(np.complex128)
    # Store history for plotting
    history = [z.copy()]
    
    frame_count = 0
    while cap.isOpened() and frame_count < num_iterations:
        ret, frame = cap.read()
        if not ret:
            cap.set(cv2.CAP_PROP_POS_FRAMES, 0)  # loop video
            ret, frame = cap.read()
        if not ret:
            break
        
        # Extract master feature from current frame
        master = extract_master_feature(frame)
        
        # Use master feature to compute a direction and step size
        # Real part controls radial movement, imag part controls rotation
        direction = np.angle(master) if np.abs(master) > 1e-6 else 0.0
        step_size = alpha * min(0.5, np.abs(master) / 128.0)  # scaled brightness
        phase = np.exp(1j * direction)
        
        # Update each root: move toward decreasing polynomial magnitude
        # Using gradient descent with master‑guided step
        for i in range(n_roots):
            z_i = z[i]
            # Compute gradient of |P(z)|^2 w.r.t. z (real and imag separately)
            p = poly_val(z_i)
            dp = poly_deriv(z_i)
            grad = 2 * np.conj(p) * dp

            if not (np.isfinite(p) and np.isfinite(dp) and np.isfinite(grad)):
                z[i] = np.exp(1j * np.angle(z_i if np.isfinite(z_i) else 1.0 + 0.0j))
                continue

            grad_mag = np.abs(grad)
            if grad_mag > 1e-12:
                grad_dir = grad / grad_mag
            else:
                grad_dir = 0.0 + 0.0j

            # Limit each step so a single frame cannot blow up the trajectory.
            clipped_step = min(step_size * grad_mag, 0.05)
            step = clipped_step * phase * grad_dir
            z_i_new = z_i - step

            # Keep estimates near the target manifold; this prevents polynomial blow-up.
            radius = np.abs(z_i_new)
            if not np.isfinite(radius) or radius < 1e-12:
                z_i_new = np.exp(1j * direction)
            else:
                radial_blend = 0.1
                z_i_new = (1.0 - radial_blend) * z_i_new + radial_blend * (z_i_new / radius)

            z[i] = z_i_new
        
        # Store history
        history.append(z.copy())
        frame_count += 1
        
        # Optional: show progress every 100 frames
        if frame_count % 100 == 0:
            err = np.mean(np.min(np.abs(z[:, None] - true_roots[None, :]), axis=1))
            print(f"Frame {frame_count}, mean error = {err:.4f}")
    
    cap.release()
    return z, history

# ------------------------------------------------------------
# Main
# ------------------------------------------------------------
if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="Follow video-derived guidance while evolving complex root estimates.")
    parser.add_argument("video_path", help="Path to the input video.")
    parser.add_argument("--alpha", type=float, default=0.005, help="Base step size.")
    parser.add_argument("--iterations", type=int, default=1000, help="Number of update frames to process.")
    parser.add_argument("--no-plot", action="store_true", help="Skip interactive plotting.")
    parser.add_argument("--save-plot", help="Optional output path for the convergence figure.")
    args = parser.parse_args()
    
    print(f"Processing {args.video_path}...")
    final_roots, history = solve_roots_with_video(args.video_path, alpha=args.alpha, num_iterations=args.iterations)
    
    # Compute final error
    errors = np.min(np.abs(final_roots[:, None] - true_roots[None, :]), axis=1)
    print(f"\nFinal mean error: {np.mean(errors):.6f}")
    print("Final roots:")
    for i, r in enumerate(final_roots):
        print(f"  {i+1}: {r:.4f}")
    
    # Plot convergence
    plt.figure(figsize=(12,5))
    plt.subplot(1,2,1)
    for i in range(n_roots):
        traj = np.array([h[i] for h in history])
        plt.plot(traj.real, traj.imag, alpha=0.6, linewidth=0.8)
    plt.scatter(true_roots.real, true_roots.imag, c='red', marker='o', label='True roots')
    plt.scatter(final_roots.real, final_roots.imag, c='blue', marker='x', label='Final estimates')
    plt.legend()
    plt.title('Root trajectories guided by video')
    plt.axis('equal')
    
    plt.subplot(1,2,2)
    errors_over_time = [np.mean(np.min(np.abs(h[:, None] - true_roots[None, :]), axis=1)) for h in history]
    plt.plot(errors_over_time)
    plt.yscale('log')
    plt.xlabel('Frame number')
    plt.ylabel('Mean error (log scale)')
    plt.title('Convergence driven by video master feature')
    plt.grid(alpha=0.3)

    if args.save_plot:
        plt.savefig(args.save_plot, dpi=150, bbox_inches="tight")

    backend = plt.get_backend().lower()
    non_interactive_backend = "agg" in backend or "pdf" in backend or "svg" in backend
    headless = not os.environ.get("DISPLAY") and not os.environ.get("WAYLAND_DISPLAY")
    if args.no_plot or headless or non_interactive_backend:
        plt.close()
    else:
        plt.show()
