# ============================
# camera.py — Orbit Camera System
# ============================
from __future__ import annotations
from dataclasses import dataclass, field
import math
import numpy as np
from typing import Optional

def normalize(v: np.ndarray) -> np.ndarray:
    """Returns the normalized vector v."""
    n = float(np.linalg.norm(v))
    return v / (n if n > 1e-12 else 1.0)

def rodrigues(v: np.ndarray, axis: np.ndarray, angle: float) -> np.ndarray:
    """Rotates vector v around axis by angle (radians) using Rodrigues' formula."""
    k = normalize(axis)
    c = math.cos(angle)
    s = math.sin(angle)
    return v * c + np.cross(k, v) * s + k * np.dot(k, v) * (1.0 - c)

WORLD_UP = np.array([0, 0, 1], dtype=np.float32)

@dataclass
class OrbitCamera:
    """
    Standard Orbit Camera for molecular visualization.
    Handles yaw, pitch, panning, and isometric snapping.
    """
    center: np.ndarray
    offset: np.ndarray # Vector from center to eye
    fov: float = 45.0
    ortho_scale: float = 20.0
    
    # Physics/Momentum
    yaw_v: float = 0.0
    pitch_v: float = 0.0
    
    # Camera Shift (Pan)
    pan: np.ndarray = field(default_factory=lambda: np.zeros(2, dtype=np.float32))

    def get_pan_vector(self) -> np.ndarray:
        # Convert 2D pan (X, Y) to 3D vector in camera basis
        right = self.right()
        # Up vector relative to camera view (Screen Up)
        up = np.cross(right, self.forward())
        return right * self.pan[0] + up * self.pan[1]

    def eye(self) -> np.ndarray:
        # Eye is center + offset + pan (in camera plane)
        return self.center + self.offset + self.get_pan_vector()

    def target(self) -> np.ndarray:
        # Target is center + pan
        return self.center + self.get_pan_vector()

    def forward(self) -> np.ndarray:
        return normalize(-self.offset)

    def right(self) -> np.ndarray:
        return normalize(np.cross(self.forward(), WORLD_UP))

    def rotate_yaw(self, angle: float):
        self.offset = rodrigues(self.offset, WORLD_UP, angle)

    def rotate_pitch(self, angle: float):
        """Robust pitch rotation with clamping to avoid gimbal lock."""
        forward = normalize(-self.offset)
        right = normalize(np.cross(forward, WORLD_UP))
        
        up_dot = np.dot(forward, WORLD_UP)
        # Current elevation angle from horizon (0) to pole (pi/2)
        max_elev = math.radians(89.0)
        cur_elev = math.asin(np.clip(up_dot, -1.0, 1.0))
        
        new_elev = cur_elev + angle
        if new_elev > max_elev:
            angle = max_elev - cur_elev
        elif new_elev < -max_elev:
            angle = -max_elev - cur_elev
            
        self.offset = rodrigues(self.offset, right, angle)

    def get_view_matrix(self) -> np.ndarray:
        from .renderer.utils import look_at
        return look_at(self.eye(), self.target(), WORLD_UP)
        
    def get_projection_matrix(self, aspect: float, is_ortho: bool = False) -> np.ndarray:
        from .renderer.utils import perspective, ortho
        if is_ortho:
            s = self.ortho_scale / 2.0
            return ortho(-s * aspect, s * aspect, -s, s, -5000.0, 5000.0)
        else:
            return perspective(self.fov, aspect, 0.1, 5000.0)

    def snap_isometric(self):
        """Snaps camera to a perfect isometric view (45 deg yaw, 35.264 deg pitch)."""
        dist = np.linalg.norm(self.offset)
        iso_pitch = math.radians(35.264)
        c, s = math.cos(iso_pitch), math.sin(iso_pitch)
        v = np.array([0, -dist*c, dist*s], dtype=np.float32)
        rot_z = math.radians(-45.0)
        v = rodrigues(v, np.array([0, 0, 1], dtype=np.float32), rot_z)
        self.offset = v
