//! Flycam: freie Debug-/Entwicklungskamera (Schritt 4). Position, Yaw und
//! Pitch in f32 — Port der irl3d-Kamera-Idee, aber ohne Q16.16: an dieser
//! Kamera hängt keine Physik, und der Renderer rechnet ohnehin f32.
//!
//! Konvention wie `math::Mat4::view`: yaw=0 blickt -Z, positiver Yaw dreht
//! nach links, positiver Pitch hebt den Blick.

use crate::render::math::Mat4;

/// Knapp unter 90°: hält den Blick aus der Senkrechten, wo Yaw bedeutungslos
/// würde (und `right`/`forward` zusammenfielen).
const PITCH_LIMIT: f32 = 1.55;

pub struct Camera {
    pub pos:   [f32; 3],
    pub yaw:   f32,
    pub pitch: f32,
}

impl Camera {
    pub fn new(pos: [f32; 3]) -> Self {
        Self { pos, yaw: 0.0, pitch: 0.0 }
    }

    /// Roh-Maus-Delta (Pixel) → Yaw/Pitch. `sens` in Radiant pro Pixel.
    /// Maus nach rechts dreht den Blick nach rechts (Yaw sinkt), Maus nach
    /// unten senkt ihn; Pitch wird geclamped.
    pub fn apply_mouse(&mut self, dx: f32, dy: f32, sens: f32) {
        self.yaw   -= dx * sens;
        self.pitch = (self.pitch - dy * sens).clamp(-PITCH_LIMIT, PITCH_LIMIT);
    }

    /// Blickrichtung inkl. Pitch — W/S bewegen sich hier entlang.
    fn forward(&self) -> [f32; 3] {
        let (sy, cy) = self.yaw.sin_cos();
        let (sp, cp) = self.pitch.sin_cos();
        [-sy * cp, sp, -cy * cp]
    }

    /// Horizontales Rechts (ohne Pitch) — A/D strafen hier entlang, ohne
    /// dass der Blick nach oben/unten die Bewegung kippt.
    fn right(&self) -> [f32; 3] {
        let (sy, cy) = self.yaw.sin_cos();
        [cy, 0.0, -sy]
    }

    /// Bewegung integrieren. `fwd`/`right`/`up` je in [-1, 1] (Tastenachsen),
    /// `dist` ist die Strecke diesen Frame (Speed · dt). Hoch/runter ist
    /// Welt-Y, damit Auf-/Absteigen unabhängig vom Blickwinkel bleibt.
    pub fn translate(&mut self, fwd: f32, right: f32, up: f32, dist: f32) {
        let f = self.forward();
        let r = self.right();
        for i in 0..3 {
            self.pos[i] += (f[i] * fwd + r[i] * right) * dist;
        }
        self.pos[1] += up * dist;
    }

    pub fn view(&self) -> Mat4 {
        Mat4::view(self.pos, self.yaw, self.pitch)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Die Bewegungs-Basis (`forward`) muss exakt zu der Richtung passen,
    /// die `view()` als „vor der Kamera" definiert — sonst driften
    /// Steuerung und Bild auseinander. Ein Punkt `d` vor der Kamera muss
    /// in View-Space auf (0, 0, -d) landen.
    #[test]
    fn forward_matches_view() {
        for &(yaw, pitch) in &[(0.0, 0.0), (0.7, 0.0), (0.0, 0.5), (1.2, -0.4)] {
            let c = Camera { pos: [1.0, 2.0, 3.0], yaw, pitch };
            let f = c.forward();
            let ahead = [
                c.pos[0] + f[0] * 5.0,
                c.pos[1] + f[1] * 5.0,
                c.pos[2] + f[2] * 5.0,
                1.0,
            ];
            let v = c.view().transform(ahead);
            assert!(
                (v[0]).abs() < 1e-4 && (v[1]).abs() < 1e-4 && (v[2] + 5.0).abs() < 1e-4,
                "yaw={yaw} pitch={pitch}: {v:?}",
            );
        }
    }

    /// `right` muss horizontal sein und im View-Space nach +X zeigen
    /// (sonst wären A/D vertauscht oder gekippt).
    #[test]
    fn right_is_horizontal_and_points_view_right() {
        let c = Camera { pos: [0.0, 0.0, 0.0], yaw: 0.6, pitch: 0.3 };
        let r = c.right();
        assert!(r[1].abs() < 1e-6, "right darf kein Y haben: {r:?}");
        let p = [r[0] * 5.0, r[1] * 5.0, r[2] * 5.0, 1.0];
        let v = c.view().transform(p);
        assert!(v[0] > 4.0, "right sollte nach View-+X zeigen: {v:?}");
    }
}
