# ============================
# utils.py — OpenGL Utility Functions & Matrix Math
# ============================
from OpenGL.GL import *
import numpy as np

def link_program(vs_src: str, fs_src: str):
    """Compiles and links a shader program from vertex and fragment shader sources."""
    vs = glCreateShader(GL_VERTEX_SHADER)
    glShaderSource(vs, vs_src)
    glCompileShader(vs)
    if not glGetShaderiv(vs, GL_COMPILE_STATUS):
        raise RuntimeError(f"VS Compile Error:\n{glGetShaderInfoLog(vs).decode()}")
        
    fs = glCreateShader(GL_FRAGMENT_SHADER)
    glShaderSource(fs, fs_src)
    glCompileShader(fs)
    if not glGetShaderiv(fs, GL_COMPILE_STATUS):
        raise RuntimeError(f"FS Compile Error:\n{glGetShaderInfoLog(fs).decode()}")
        
    prog = glCreateProgram()
    glAttachShader(prog, vs)
    glAttachShader(prog, fs)
    glLinkProgram(prog)
    if not glGetProgramiv(prog, GL_LINK_STATUS):
        raise RuntimeError(f"Link Error:\n{glGetProgramInfoLog(prog).decode()}")
        
    glDeleteShader(vs)
    glDeleteShader(fs)
    return prog

# --- Matrix Math Helpers (Numpy) ---

def normalize(v: np.ndarray) -> np.ndarray:
    n = np.linalg.norm(v)
    return v / n if n > 1e-12 else v

def look_at(eye: np.ndarray, target: np.ndarray, up: np.ndarray) -> np.ndarray:
    f = normalize(target - eye)
    s = normalize(np.cross(f, up))
    u = np.cross(s, f)
    res = np.eye(4, dtype=np.float32)
    res[0, :3] = s
    res[1, :3] = u
    res[2, :3] = -f
    res[0, 3] = -np.dot(s, eye)
    res[1, 3] = -np.dot(u, eye)
    res[2, 3] = np.dot(f, eye)
    return res

def perspective(fovy: float, aspect: float, near: float, far: float) -> np.ndarray:
    import math
    f = 1.0 / math.tan(math.radians(fovy) / 2.0)
    res = np.zeros((4, 4), dtype=np.float32)
    res[0, 0] = f / aspect
    res[1, 1] = f
    res[2, 2] = (far + near) / (near - far)
    res[3, 2] = -1.0
    res[2, 3] = (2.0 * far * near) / (near - far)
    return res

def ortho(left: float, right: float, bottom: float, top: float, near: float, far: float) -> np.ndarray:
    res = np.eye(4, dtype=np.float32)
    res[0, 0] = 2.0 / (right - left)
    res[1, 1] = 2.0 / (top - bottom)
    res[2, 2] = -2.0 / (far - near)
    res[0, 3] = -(right + left) / (right - left)
    res[1, 3] = -(top + bottom) / (top - bottom)
    res[2, 3] = -(far + near) / (far - near)
    return res
