//! Fenster-Frontend: winit-Loop um den wgpu-Renderer.
//!
//! Stand: Schritt 4 des Renderer-Plans (notes/renderer-plan.md) — Flycam
//! über dem PS1-Szenen-Pass. WASD bewegt, Maus blickt (Klick fängt den
//! Cursor, Escape gibt ihn frei bzw. beendet), Space/Shift hoch/runter.
//! Als Nächstes: OBJ-Szene (Schritt 5).
//!
//! Steuert dieselbe [`Session`] wie die CLI: das Fenster *besitzt* sie,
//! Terminal-Befehle kommen über einen Channel von einem stdin-Thread und
//! werden pro Loop-Durchlauf eingespielt. Im Dialog-Modus pausiert die
//! Welt (Sim-Uhr und Kamera stehen) — so sieht das Fenster nie einen
//! anderen Zustand als die Konsole.
//!
//! Schicht-Regel wie `cli`: Geschwister von `engine`/`session`, konsumiert
//! deren Schnittstellen — nie umgekehrt.

mod brush;
mod camera;
mod font;
mod gpu;
mod math;
mod scene;
mod sprite;
mod ui;

use std::sync::mpsc::{self, Receiver};
use std::sync::Arc;
use std::time::Instant;

use winit::application::ApplicationHandler;
use winit::event::{DeviceEvent, DeviceId, ElementState, KeyEvent, MouseButton, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::{CursorGrabMode, Window, WindowId};

use crate::engine::tga::Image;
use crate::engine::{assets, map, tga};
use crate::session::{Mode, Session};
use camera::Camera;
use gpu::Gpu;
use scene::Mesh;

/// Radiant pro Maus-Pixel.
const MOUSE_SENS: f32 = 0.0025;
/// Welt-Einheiten pro Sekunde.
const MOVE_SPEED: f32 = 4.0;

pub fn run(mut session: Session) {
    // Init-Signal feuern, bevor das Fenster steht (kann bereits einen
    // Dialog öffnen — dann startet die Welt eben pausiert).
    for line in session.start() { println!("{line}"); }

    // CPU-Assets dekodieren (Pull über die engine-Decoder). Das Hochladen
    // auf die GPU macht später `Gpu` — Decode (CPU) und Upload (GPU) bleiben
    // getrennt.
    let world = map::load(&assets::path("assets/maps/test.map"));
    let tex_names = brush::texture_names(&world);
    let images: Vec<Image> = tex_names.iter()
        .map(|n| tga::load(&assets::path(&format!("assets/textures/{n}.tga"))))
        .collect();
    let dims: Vec<(u32, u32)> = images.iter().map(|i| (i.width, i.height)).collect();
    let mesh = brush::build(&world, &tex_names, &dims);
    report_map(&world, &tex_names, &mesh);

    // UI-Texturen, Reihenfolge = die Index-Konstanten in ui (WHITE, FONT_EGA,
    // FONT_CGA, CURSORS, ORN). Decode (CPU) bleibt in run(); Fonts, Cursor und
    // Ornament sind weiß-auf-schwarz und bekommen ihre Alpha-Maske aus der
    // Luminanz.
    let load_keyed = |p: &str| ui::key_luminance(&tga::load(&assets::path(p)));
    // Font-Atlanten zuerst keyen, daraus die Glyph-Breiten messen (Font),
    // dann dieselben Bilder in die GPU-Texturliste übernehmen.
    let ega_img = load_keyed("assets/textures/fonts/oldschool-ega-8x14.tga");
    let cga_img = load_keyed("assets/textures/fonts/oldschool-cga-8x8.tga");
    let fonts = font::Fonts {
        ega: font::Font::ega(ui::FONT_EGA, &ega_img),
        cga: font::Font::cga(ui::FONT_CGA, &cga_img),
    };
    let ui_textures: Vec<Image> = vec![
        ui::white_pixel(),
        ega_img,
        cga_img,
        load_keyed("assets/textures/ui/cursors.tga"),
        load_keyed("assets/textures/ui/panel_ornaments.tga"),
    ];

    // stdin auf einem eigenen Thread: er darf blockieren, der Main-Thread
    // (winit) nicht. Es queren nur Strings die Thread-Grenze, kein Zustand
    // — der bleibt allein auf dem Main-Thread.
    let (tx, rx) = mpsc::channel::<String>();
    std::thread::spawn(move || {
        let stdin = std::io::stdin();
        let mut buf = String::new();
        loop {
            buf.clear();
            match stdin.read_line(&mut buf) {
                Ok(0) | Err(_) => break, // EOF (Ctrl-D) → Thread endet
                Ok(_) => {
                    if tx.send(buf.trim_end().to_string()).is_err() { break; }
                }
            }
        }
    });

    let event_loop = EventLoop::new().expect("winit: Event-Loop");
    // Poll statt Wait: wir rendern kontinuierlich (Spiel, kein Editor) und
    // pollen nebenbei den Befehls-Channel.
    event_loop.set_control_flow(ControlFlow::Poll);
    let mut app = App::new(session, rx, mesh, images, ui_textures, fonts);
    event_loop.run_app(&mut app).expect("winit: run");
}

/// Kurzer Lade-Report: Map-Inhalt und resultierende Geometriegröße.
fn report_map(world: &map::Map, tex_names: &[String], mesh: &Mesh) {
    let brushes: usize = world.entities.iter().map(|e| e.brushes.len()).sum();
    let faces: usize = world.entities.iter()
        .flat_map(|e| &e.brushes)
        .map(|b| b.faces.len())
        .sum();
    println!(
        "[map] {} Entities, {} Brushes, {} Faces, Texturen {:?} → {} Vertices, {} Dreiecke",
        world.entities.len(), brushes, faces, tex_names,
        mesh.verts.len(), mesh.indices.len() / 3,
    );
}

/// Aktueller Eingabezustand. Tasten als gehaltene Flags (nicht Events),
/// damit die Bewegung pro Frame mit dt skaliert; Maus-Delta wird zwischen
/// den Frames akkumuliert und nach dem Anwenden genullt.
#[derive(Default)]
struct Input {
    fwd:      bool,
    back:     bool,
    left:     bool,
    right:    bool,
    up:       bool,
    down:     bool,
    mouse_dx: f32,
    mouse_dy: f32,
    /// Cursor gefangen → Maus-Look aktiv.
    grabbed:  bool,
}

struct App {
    window:  Option<Arc<Window>>,
    gpu:     Option<Gpu>,
    session: Session,
    rx:      Receiver<String>,
    camera:  Camera,
    input:   Input,
    last:    Option<Instant>,
    /// Letzte Mausposition in Fenster-Pixeln (für den UI-Cursor).
    cursor_win: [f32; 2],
    /// CPU-seitige Welt-Geometrie + Texturen, in `resumed` einmal auf die
    /// GPU geladen.
    mesh:        Mesh,
    images:      Vec<Image>,
    ui_textures: Vec<Image>,
    fonts:       font::Fonts,
}

impl App {
    fn new(
        session: Session,
        rx: Receiver<String>,
        mesh: Mesh,
        images: Vec<Image>,
        ui_textures: Vec<Image>,
        fonts: font::Fonts,
    ) -> Self {
        Self {
            window:  None,
            gpu:     None,
            session,
            rx,
            // Etwas zurück und erhöht, Blick Richtung Welt-Ursprung.
            camera:  Camera::new([0.0, 1.0, 4.0]),
            input:   Input::default(),
            last:    None,
            cursor_win: [0.0, 0.0],
            mesh,
            images,
            ui_textures,
            fonts,
        }
    }

    // Der OS-Cursor bleibt immer versteckt (in `resumed` einmal gesetzt) —
    // wir zeichnen stets ein eigenes Sprite. `set_grab` schaltet nur die
    // Greif-Art: Locked (Flycam, relativer Blick) ↔ None (freie Maus, UI).
    fn set_grab(&mut self, grab: bool) {
        let Some(window) = &self.window else { return; };
        if grab {
            // Locked (Cursor fixiert) ist am angenehmsten, wird aber nicht
            // überall unterstützt → auf Confined zurückfallen.
            let ok = window.set_cursor_grab(CursorGrabMode::Locked)
                .or_else(|_| window.set_cursor_grab(CursorGrabMode::Confined))
                .is_ok();
            if ok { self.input.grabbed = true; }
        } else {
            let _ = window.set_cursor_grab(CursorGrabMode::None);
            self.input.grabbed = false;
        }
    }
}

impl ApplicationHandler for App {
    /// winit liefert das Fenster erst hier, nicht beim Loop-Start —
    /// deshalb sind `window`/`gpu` Options statt Konstruktor-Felder.
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        if self.window.is_some() { return; }
        let attrs = Window::default_attributes()
            .with_title("wds")
            // 2× interne Auflösung als Startgröße; frei resizebar.
            .with_inner_size(winit::dpi::LogicalSize::new(
                gpu::INTERNAL_W * 2, gpu::INTERNAL_H * 2));
        let window = Arc::new(event_loop.create_window(attrs).expect("winit: Fenster"));
        // OS-Cursor dauerhaft aus — das Overlay zeichnet immer einen eigenen.
        window.set_cursor_visible(false);
        self.gpu = Some(Gpu::new(
            window.clone(),
            event_loop.owned_display_handle(),
            &self.mesh,
            &self.images,
            &self.ui_textures,
        ));
        self.window = Some(window);
    }

    /// Rohe Maus-Bewegung (unabhängig von Cursor-Position) — nur sinnvoll,
    /// solange der Cursor gefangen ist.
    fn device_event(&mut self, _el: &ActiveEventLoop, _id: DeviceId, event: DeviceEvent) {
        if let DeviceEvent::MouseMotion { delta } = event {
            if self.input.grabbed {
                self.input.mouse_dx += delta.0 as f32;
                self.input.mouse_dy += delta.1 as f32;
            }
        }
    }

    /// Läuft einmal pro Loop-Durchlauf, nachdem die Events abgearbeitet
    /// sind: Konsolen-Befehle einspielen und den nächsten Frame anfordern.
    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
        while let Ok(line) = self.rx.try_recv() {
            let r = self.session.exec(&line);
            for out in r.output { println!("{out}"); }
            if r.quit { event_loop.exit(); }
        }
        if let Some(w) = &self.window { w.request_redraw(); }
    }

    fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
        match event {
            WindowEvent::CloseRequested => event_loop.exit(),
            WindowEvent::CursorMoved { position, .. } => {
                self.cursor_win = [position.x as f32, position.y as f32];
            }
            WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left, .. } => {
                // Nur im freien Modus greift der Klick die Maus (Flycam). Im
                // UI-/Dialog-Modus bleibt die Maus frei (für Klicks aufs UI).
                if matches!(self.session.mode, Mode::Free) { self.set_grab(true); }
            }
            WindowEvent::KeyboardInput { event: key, .. } => self.on_key(event_loop, key),
            WindowEvent::Resized(size) => {
                if let Some(gpu) = &mut self.gpu {
                    gpu.resize(size.width, size.height);
                }
            }
            WindowEvent::RedrawRequested => self.redraw(),
            _ => {}
        }
    }
}

impl App {
    fn on_key(&mut self, event_loop: &ActiveEventLoop, key: KeyEvent) {
        let PhysicalKey::Code(code) = key.physical_key else { return; };
        let down = key.state == ElementState::Pressed;

        // Escape: erst den Cursor freigeben, sonst beenden.
        if code == KeyCode::Escape && down {
            if self.input.grabbed { self.set_grab(false); } else { event_loop.exit(); }
            return;
        }

        match code {
            KeyCode::KeyW => self.input.fwd   = down,
            KeyCode::KeyS => self.input.back  = down,
            KeyCode::KeyA => self.input.left  = down,
            KeyCode::KeyD => self.input.right = down,
            KeyCode::Space      => self.input.up   = down,
            KeyCode::ShiftLeft  => self.input.down = down,
            _ => {}
        }
    }

    fn redraw(&mut self) {
        // dt messen; im Dialog Kamera und Sim anhalten → Welt pausiert,
        // während die Konsole den Dialog treibt.
        let now = Instant::now();
        let dt = self.last.replace(now).map_or(0.0, |prev| (now - prev).as_secs_f32());

        if matches!(self.session.mode, Mode::Free) {
            self.camera.apply_mouse(self.input.mouse_dx, self.input.mouse_dy, MOUSE_SENS);
            let fwd   = axis(self.input.fwd,   self.input.back);
            let right = axis(self.input.right, self.input.left);
            let up    = axis(self.input.up,    self.input.down);
            self.camera.translate(fwd, right, up, MOVE_SPEED * dt);
        } else if self.input.grabbed {
            // Nicht-Free (Dialog/Menü) → Maus freigeben, damit das UI klickbar
            // ist und der Maus-Cursor statt des HUD-Pointers erscheint.
            self.set_grab(false);
        }
        // Delta immer verwerfen, auch im Dialog — sonst springt der Blick
        // beim Fortsetzen um die aufgestaute Bewegung.
        self.input.mouse_dx = 0.0;
        self.input.mouse_dy = 0.0;

        let view = self.camera.view();
        // Mausposition über die Letterbox in interne Pixel mappen.
        let cursor = ui::Cursor {
            pos: self.gpu.as_ref()
                .map(|g| g.map_cursor(self.cursor_win))
                .unwrap_or([0.0, 0.0]),
            grabbed: self.input.grabbed,
        };
        // Overlay state-driven bauen (Stage 3: Demo-Panel + Cursor).
        let overlay = ui::layout(
            [gpu::INTERNAL_W as f32, gpu::INTERNAL_H as f32], &self.fonts, &cursor,
        );
        if let Some(gpu) = &mut self.gpu { gpu.frame(&view, &overlay); }
    }
}

/// Zwei gegensätzliche Tasten zu einer Achse in {-1, 0, 1}.
fn axis(positive: bool, negative: bool) -> f32 {
    (positive as i32 - negative as i32) as f32
}
