//! The application layer: both front doors into the engine.
//!
//! Door 1 (the XNA-shaped on-ramp): implement [`Game`] on your state struct
//! and call [`run`]. Your code lives in `init` / `update` / `draw`.
//!
//! Door 2 (the defining model, learning path step 8): build an [`App`] with
//! the consuming builder, seed the [`World`] with `spawn_with`, register
//! systems (plain `fn(&mut World, f32)`), and call `App::run`. The v0.1
//! scheduler is a visible for loop; render systems record draws into the
//! [`RenderQueue`] resource and the runner replays them through the seams
//! (camera per draw call, present only inside `end_frame`).
//!
//! Both doors share the winit plumbing below. winit 0.30.13 (verified
//! 2026-06-27); the key-mapping table is a pragmatic subset, extend as needed.

use std::sync::Arc;

use winit::application::ApplicationHandler;
use winit::event::{ElementState, MouseScrollDelta, WindowEvent};
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::{Window, WindowId};

use crate::camera::{Camera, OrthographicCamera};
use crate::color::Color;
use crate::content::{ContentManager, TextureId};
use crate::drawable::{Drawable, Sprite};
use crate::error::Result;
use crate::gfx::wgpu_backend::WgpuBackend;
use crate::gfx::RenderBackend;
use crate::input::{InputState, Key, MouseButton};
use crate::math::Vec2;
use crate::module::Module;
use crate::time::{Clock, GameTime};
use crate::world::World;

/// Window/app configuration passed to [`run`].
#[derive(Debug, Clone)]
pub struct AppConfig {
    pub title: String,
    pub width: u32,
    pub height: u32,
    /// If true, pressing Escape closes the window.
    pub exit_on_escape: bool,
    /// Which graphics backend(s) wgpu may use. Force `Backends::VULKAN` if you
    /// intend to reach raw Vulkan handles for OpenXR later (see handoff.md).
    pub backends: wgpu::Backends,
}

impl Default for AppConfig {
    fn default() -> Self {
        Self {
            title: "Mawida".to_string(),
            width: 800,
            height: 600,
            exit_on_escape: true,
            backends: wgpu::Backends::all(),
        }
    }
}

/// Carries the engine's runtime services into your callbacks. The XNA `Game`
/// base-class state, made explicit instead of inherited.
pub struct Context {
    backend: WgpuBackend,
    content: ContentManager,
    input: InputState,
    #[cfg(feature = "gamepad")]
    gamepad: crate::gamepad::GamepadPump,
    clear: Color,
    exit: bool,
}

impl Context {
    /// Read-only access to this frame's input snapshot.
    pub fn input(&self) -> &InputState {
        &self.input
    }

    /// This frame's controller snapshot (`gamepad` feature). Polled once
    /// per frame by the runner; same edge semantics as the keyboard.
    #[cfg(feature = "gamepad")]
    pub fn gamepad(&mut self) -> &crate::gamepad::GamepadState {
        self.gamepad.poll()
    }

    /// Drawable surface size in physical pixels.
    pub fn size(&self) -> (u32, u32) {
        self.backend.size()
    }

    /// Set the color the screen is cleared to at the start of `draw`.
    pub fn set_clear_color(&mut self, color: Color) {
        self.clear = color;
    }

    /// Decode an image file and upload it to the GPU, returning a handle.
    /// Do this in `init` or on level load, not per frame.
    pub fn load_texture(&mut self, path: impl AsRef<std::path::Path>) -> Result<TextureId> {
        let id = self.content.load_texture(path)?;
        let data = self.content.get(id)?.clone();
        self.backend.upload_texture(id, &data);
        Ok(id)
    }

    /// Direct access to the content manager, for code that builds assets in
    /// memory (an [`crate::content::AtlasBuilder`], procedural textures, a
    /// generated glyph atlas). Anything inserted here still needs
    /// [`upload_texture`](Context::upload_texture) before it can be drawn.
    pub fn content_mut(&mut self) -> &mut ContentManager {
        &mut self.content
    }

    /// Upload a texture already registered in the content manager to the
    /// GPU. `load_texture` does both steps in one call for files; this is
    /// the second step for in-memory assets.
    pub fn upload_texture(&mut self, id: TextureId) -> Result<()> {
        let data = self.content.get(id)?.clone();
        self.backend.upload_texture(id, &data);
        Ok(())
    }

    /// Queue a drawable for this frame, viewed through `camera`. Call this from
    /// your [`Game::draw`]. Calling with different cameras (e.g. one per eye) is
    /// supported.
    pub fn draw(&mut self, drawable: &dyn Drawable, camera: &dyn Camera) -> Result<()> {
        self.backend.draw(drawable, camera)
    }

    /// Point the directional light the Lit material uses (v0.3).
    /// `direction` is FROM the light, world space, normalized for you.
    pub fn set_light(&mut self, direction: crate::math::Vec3, color: Color) {
        self.backend.set_light(direction, color);
    }

    /// Request that the application close after this frame.
    pub fn exit(&mut self) {
        self.exit = true;
    }
}

/// The asset services the ECS door sees during loading (item 4 of the
/// v0.4 sweep): the content manager and the GPU upload step, borrowed
/// together. Passed to [`App::load_with`] closures once the GPU is up;
/// what you load becomes component data (TextureIds, fonts, models).
pub struct Assets<'a> {
    content: &'a mut ContentManager,
    backend: &'a mut WgpuBackend,
}

impl Assets<'_> {
    /// Decode an image file and upload it: one call, like Context's.
    pub fn load_texture(&mut self, path: impl AsRef<std::path::Path>) -> Result<TextureId> {
        let id = self.content.load_texture(path)?;
        let data = self.content.get(id)?.clone();
        self.backend.upload_texture(id, &data);
        Ok(id)
    }

    /// Register raw RGBA8 pixels and upload them (procedural textures,
    /// atlas canvases).
    pub fn insert_texture(&mut self, width: u32, height: u32, rgba: Vec<u8>) -> TextureId {
        let id = self.content.insert_rgba(width, height, rgba);
        if let Ok(data) = self.content.get(id) {
            let data = data.clone();
            self.backend.upload_texture(id, &data);
        }
        id
    }

    /// The raw content manager, for AtlasBuilder-style flows; pair with
    /// [`upload_texture`](Assets::upload_texture).
    pub fn content_mut(&mut self) -> &mut ContentManager {
        self.content
    }

    pub fn upload_texture(&mut self, id: TextureId) -> Result<()> {
        let data = self.content.get(id)?.clone();
        self.backend.upload_texture(id, &data);
        Ok(())
    }

    /// The built-in 8x8 bitmap font, registered AND uploaded: drop the
    /// returned font into a World resource and the `render_text` system
    /// can draw `Text` components with it.
    pub fn builtin_font(&mut self) -> Result<crate::text::MonoFont> {
        let font = crate::text::MonoFont::builtin(self.content);
        self.upload_texture(font.sheet.texture)?;
        Ok(font)
    }

    /// Load a glTF model and upload its base-color texture if it has one.
    #[cfg(feature = "gltf")]
    pub fn load_gltf(
        &mut self,
        path: impl AsRef<std::path::Path>,
    ) -> Result<crate::content::LoadedModel> {
        let model = self.content.load_gltf(path)?;
        if let Some(tex) = model.material.texture {
            self.upload_texture(tex)?;
        }
        Ok(model)
    }
}

/// Implement this on your game state. Only [`update`](Game::update) and
/// [`draw`](Game::draw) are required.
pub trait Game {
    /// One-time setup after the GPU is ready. Load assets here.
    fn init(&mut self, _ctx: &mut Context) -> Result<()> {
        Ok(())
    }

    /// Advance simulation by `time.delta`. Read input via `ctx.input()`.
    fn update(&mut self, ctx: &mut Context, time: GameTime) -> Result<()>;

    /// Issue draw calls via `ctx.draw(..)`. The screen is already cleared.
    fn draw(&mut self, ctx: &mut Context) -> Result<()>;
}

/// Start the game. Blocks until the window closes.
pub fn run(game: impl Game + 'static, config: AppConfig) -> Result<()> {
    // Best-effort logging; ignored if env_logger is already initialized.
    let _ = env_logger::try_init();

    let event_loop = EventLoop::new()
        .map_err(|e| crate::error::MawidaError::Other(format!("event loop: {e}")))?;

    let mut runner = Runner {
        game: Some(Box::new(game)),
        config,
        window: None,
        ctx: None,
        clock: Clock::new(),
        boxed_game: None,
        occluded: false,
    };

    event_loop
        .run_app(&mut runner)
        .map_err(|e| crate::error::MawidaError::Other(format!("run_app: {e}")))?;
    Ok(())
}

/// Create the window and bring up the wgpu backend, or exit the event loop.
/// Shared by both runners so there is exactly one bring-up path.
fn init_window_backend(
    event_loop: &ActiveEventLoop,
    config: &AppConfig,
) -> Option<(Arc<Window>, WgpuBackend)> {
    let attrs = Window::default_attributes()
        .with_title(&config.title)
        .with_inner_size(winit::dpi::LogicalSize::new(config.width, config.height));

    let window = match event_loop.create_window(attrs) {
        Ok(w) => Arc::new(w),
        Err(e) => {
            log::error!("failed to create window: {e}");
            event_loop.exit();
            return None;
        }
    };

    let backend = match WgpuBackend::new(window.clone(), config.backends) {
        Ok(b) => b,
        Err(e) => {
            log::error!("failed to init wgpu backend: {e}");
            event_loop.exit();
            return None;
        }
    };

    Some((window, backend))
}

struct Runner {
    // `game` holds the not-yet-started game until `resumed`; then it moves to
    // `boxed_game` alongside the live `ctx`. Two slots keep the borrow simple.
    game: Option<Box<dyn Game>>,
    boxed_game: Option<Box<dyn Game>>,
    config: AppConfig,
    window: Option<Arc<Window>>,
    ctx: Option<Context>,
    clock: Clock,
    occluded: bool,
}

impl ApplicationHandler for Runner {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        if self.window.is_some() {
            return; // already initialized (resumed can fire more than once)
        }

        let Some((window, backend)) = init_window_backend(event_loop, &self.config) else {
            return;
        };

        let mut ctx = Context {
            backend,
            content: ContentManager::new(),
            input: InputState::new(),
            #[cfg(feature = "gamepad")]
            gamepad: crate::gamepad::GamepadPump::new(),
            clear: crate::CORNFLOWER_BLUE,
            exit: false,
        };

        let mut game = self.game.take().expect("game present at resume");
        if let Err(e) = game.init(&mut ctx) {
            log::error!("game init failed: {e}");
            event_loop.exit();
            return;
        }

        self.window = Some(window);
        self.ctx = Some(ctx);
        self.boxed_game = Some(game);
    }

    fn window_event(
        &mut self,
        event_loop: &ActiveEventLoop,
        _id: WindowId,
        event: WindowEvent,
    ) {
        let (Some(ctx), Some(game)) = (self.ctx.as_mut(), self.boxed_game.as_mut()) else {
            return;
        };

        match event {
            WindowEvent::CloseRequested => event_loop.exit(),

            // The fully-occluded fix: a hidden window used to busy-spin at
            // ~100% CPU (request_redraw -> acquire says Occluded -> skip ->
            // request again). Track occlusion and pause the redraw loop.
            WindowEvent::Occluded(occluded) => {
                self.occluded = occluded;
                if !occluded {
                    if let Some(w) = &self.window {
                        w.request_redraw(); // wake the loop back up
                    }
                }
            }

            WindowEvent::Resized(size) => {
                ctx.backend.resize(size.width, size.height);
            }

            WindowEvent::KeyboardInput { event, .. } => {
                if let PhysicalKey::Code(code) = event.physical_key {
                    if let Some(key) = map_key(code) {
                        match event.state {
                            ElementState::Pressed => ctx.input.press_key(key),
                            ElementState::Released => ctx.input.release_key(key),
                        }
                    }
                    if self.config.exit_on_escape
                        && code == KeyCode::Escape
                        && event.state == ElementState::Pressed
                    {
                        event_loop.exit();
                    }
                }
            }

            WindowEvent::MouseInput { state, button, .. } => {
                if let Some(b) = map_mouse(button) {
                    match state {
                        ElementState::Pressed => ctx.input.press_mouse(b),
                        ElementState::Released => ctx.input.release_mouse(b),
                    }
                }
            }

            WindowEvent::CursorMoved { position, .. } => {
                ctx.input.mouse_position = Vec2::new(position.x as f32, position.y as f32);
            }

            WindowEvent::MouseWheel { delta, .. } => {
                ctx.input.scroll_delta += match delta {
                    MouseScrollDelta::LineDelta(_, y) => y,
                    MouseScrollDelta::PixelDelta(p) => p.y as f32,
                };
            }

            WindowEvent::RedrawRequested => {
                let time = self.clock.tick();

                // update
                if let Err(e) = game.update(ctx, time) {
                    log::error!("update error: {e}");
                    event_loop.exit();
                    return;
                }

                // render: clear -> game draw calls -> present
                let clear = ctx.clear;
                if let Err(e) = ctx.backend.begin_frame(clear) {
                    log::error!("begin_frame error: {e}");
                }
                if let Err(e) = game.draw(ctx) {
                    log::error!("draw error: {e}");
                }
                if let Err(e) = ctx.backend.end_frame() {
                    log::error!("end_frame error: {e}");
                }

                // prepare input edges for next frame
                ctx.input.begin_frame();

                if ctx.exit {
                    event_loop.exit();
                }
            }

            _ => {}
        }
    }

    fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
        // Drive a continuous render loop, unless the window is fully
        // hidden: presenting is what paces the loop (vsync), and an
        // occluded surface never presents, so requesting redraws while
        // hidden spins the CPU for nothing.
        if self.occluded {
            return;
        }
        if let Some(window) = &self.window {
            window.request_redraw();
        }
    }
}

// ---------------------------------------------------------------------------
// Door 2: the App facade over the ECS (learning path step 8)
// ---------------------------------------------------------------------------

/// The directional light as World data (ECS door): insert or mutate this
/// resource and the runner forwards it to the lit pipeline each frame.
/// (Game-trait door: call `ctx.set_light(..)` instead.)
#[derive(Debug, Clone, Copy)]
pub struct DirectionalLight {
    /// FROM the light, world space.
    pub direction: crate::math::Vec3,
    pub color: Color,
}

/// The entire "system framework," one line: a system is a stored callable
/// with the uniform shape `fn(&mut World, f32)`. Both a named function and a
/// capturing closure fit in the box.
pub type System = Box<dyn Fn(&mut World, f32)>;

/// A one-shot setup callable: may MOVE data into the World, runs exactly
/// once. Boxed for storage, like [`System`], but `FnOnce` is the honest
/// calling contract (re-callability would be both wrong and restrictive).
pub type SetupFn = Box<dyn FnOnce(&mut World)>;

/// A one-shot asset loader for the ECS door: runs once when the GPU is up,
/// BEFORE the setup closures, so setup can spawn entities carrying the
/// handles it produced (via World resources or captured moves).
pub type LoaderFn = Box<dyn FnOnce(&mut Assets, &mut World)>;

/// Recorded draws as plain data. Lives in the World as a resource.
///
/// Render systems have the same signature as every other system; what makes
/// one a render system is only what data it writes: entries here. The runner
/// replays them between `begin_frame` and `end_frame`, camera per call
/// (seam 2 and the per-eye VR seam), and no system can ever present (seam 4).
#[derive(Default)]
pub struct RenderQueue {
    /// drawable + the camera to view it through, one entry per draw
    pub sprites: Vec<(Sprite, OrthographicCamera)>,
}

/// The builder facade over the ECS: seed the [`World`], register systems,
/// run. Each method takes the App by value (`mut self`) and returns it by
/// move, so the chain reads like configuration and `run` consumes the App
/// for good: after `run`, there is no App left to misuse.
///
/// ```no_run
/// # use mawida::{App, AppConfig, World};
/// fn setup(world: &mut World) { /* spawn entities */ }
/// fn movement(world: &mut World, dt: f32) { /* a system */ }
///
/// App::new(AppConfig::default())
///     .spawn_with(setup)
///     .add_system(movement)
///     .run()
///     .unwrap();
/// ```
pub struct App {
    config: AppConfig,
    world: World,
    setup: Vec<SetupFn>,                  // run once, then gone
    loaders: Vec<LoaderFn>,               // asset loading: once, GPU up, before setup
    systems: Vec<System>,                 // run every frame, in order
    fixed_systems: Vec<System>,           // run on the FIXED clock (item 8)
    fixed_dt: f32,                        // the fixed step, seconds (default 1/60)
    accumulator: f32,                     // time owed to the fixed systems
    modules: Vec<Box<dyn Module>>,        // the registry: owned trait objects
    content: ContentManager,
    // -- runtime state, None/default until `run` brings the window up --
    window: Option<Arc<Window>>,
    backend: Option<WgpuBackend>,
    input: InputState,
    #[cfg(feature = "gamepad")]
    gamepad: crate::gamepad::GamepadPump,
    clock: Clock,
    occluded: bool,
}

impl App {
    pub fn new(config: AppConfig) -> Self {
        Self {
            config,
            world: World::new(),
            setup: Vec::new(),
            loaders: Vec::new(),
            systems: Vec::new(),
            fixed_systems: Vec::new(),
            fixed_dt: 1.0 / 60.0,
            accumulator: 0.0,
            modules: Vec::new(),
            content: ContentManager::new(),
            window: None,
            backend: None,
            input: InputState::new(),
            #[cfg(feature = "gamepad")]
            gamepad: crate::gamepad::GamepadPump::new(),
            clock: Clock::new(),
            occluded: false,
        }
    }

    /// Register a system. `impl Fn(..) + 'static` accepts any concrete
    /// callable (named fn or closure); we box it once here. Registration
    /// order IS execution order in v0.1.
    pub fn add_system(mut self, system: impl Fn(&mut World, f32) + 'static) -> Self {
        self.systems.push(Box::new(system));
        self // returned by move: chaining
    }

    /// Register a one-time setup closure. `FnOnce`: setup may MOVE data into
    /// the World, and runs exactly once, after the GPU and window are up.
    pub fn spawn_with(mut self, setup: impl FnOnce(&mut World) + 'static) -> Self {
        self.setup.push(Box::new(setup));
        self
    }

    /// Register a system on the FIXED clock (item 8): it always receives
    /// exactly `fixed_dt` (default 1/60 s), however long the render frame
    /// took. Gaffer's accumulator, grown from the sketch in the step 8
    /// guide: physics-ish systems go here for determinism; input-reading
    /// and render systems stay on the variable clock.
    pub fn add_fixed_system(mut self, system: impl Fn(&mut World, f32) + 'static) -> Self {
        self.fixed_systems.push(Box::new(system));
        self
    }

    /// Override the fixed step (seconds per tick). 1/60 by default.
    pub fn fixed_dt(mut self, dt: f32) -> Self {
        assert!(dt > 0.0, "fixed_dt must be positive");
        self.fixed_dt = dt;
        self
    }

    /// Register a one-time asset loader (ECS door, item 4): runs with the
    /// GPU up, before `spawn_with` closures. Load textures/fonts/models
    /// here and park the handles in World resources for systems to read.
    pub fn load_with(mut self, loader: impl FnOnce(&mut Assets, &mut World) + 'static) -> Self {
        self.loaders.push(Box::new(loader));
        self
    }

    /// XNA-style content root for the ECS door's asset loading.
    pub fn content_root(mut self, root: impl Into<std::path::PathBuf>) -> Self {
        self.content.root = Some(root.into());
        self
    }

    /// Register a module (step 9). The concrete type is erased here, once:
    /// after the push the registry only knows `dyn Module`, exactly like
    /// holding a `unique_ptr<IModule>` in C++.
    pub fn add_module(mut self, module: impl Module + 'static) -> Self {
        self.modules.push(Box::new(module));
        self
    }

    /// Start the app. Blocks until the window closes. Owns the winit loop.
    /// The App IS the winit handler: `run` moves `self` into the loop.
    pub fn run(mut self) -> Result<()> {
        let _ = env_logger::try_init();

        let event_loop = EventLoop::new()
            .map_err(|e| crate::error::MawidaError::Other(format!("event loop: {e}")))?;

        event_loop
            .run_app(&mut self)
            .map_err(|e| crate::error::MawidaError::Other(format!("run_app: {e}")))?;
        Ok(())
    }
}

/// The winit driving of the ECS door. Same plumbing as [`Runner`], but the
/// frame body is: publish input as data, tick the modules, run the
/// scheduler, replay the render queue through the seams.
impl ApplicationHandler for App {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        if self.window.is_some() {
            return; // resumed can fire more than once
        }

        let Some((window, backend)) = init_window_backend(event_loop, &self.config) else {
            return;
        };

        // The World gets its startup resources: the camera and the render
        // queue go in as data, so systems can reach them like anything else.
        let (w, h) = backend.size();
        self.world
            .insert_resource(OrthographicCamera::new(w as f32, h as f32));
        self.world.insert_resource(RenderQueue::default());

        self.window = Some(window);
        self.backend = Some(backend);

        // Modules init once, in registration order, with the GPU up.
        // The mem::take dance: calling m.init(self) while iterating
        // &mut self.modules would borrow self twice, so move the list out
        // (leaving an empty Vec), iterate the local copy, put it back.
        let mut modules = std::mem::take(&mut self.modules);
        for m in &mut modules {
            m.init(self);
        }
        self.modules = modules;

        // Asset loaders run first (they need the backend for uploads)...
        let loaders: Vec<LoaderFn> = self.loaders.drain(..).collect();
        if let Some(backend) = self.backend.as_mut() {
            let mut assets = Assets {
                content: &mut self.content,
                backend,
            };
            for loader in loaders {
                loader(&mut assets, &mut self.world);
            }
        }

        // ...then the setup closures run once against the fresh World.
        for setup in self.setup.drain(..) {
            setup(&mut self.world); // Box<dyn FnOnce>: callable exactly once
        }
    }

    fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
        let Some(backend) = self.backend.as_mut() else {
            return;
        };

        match event {
            WindowEvent::CloseRequested => event_loop.exit(),

            // Same occluded fix as the Game-trait runner: pause redraws
            // while fully hidden, resume on reveal.
            WindowEvent::Occluded(occluded) => {
                self.occluded = occluded;
                if !occluded {
                    if let Some(w) = &self.window {
                        w.request_redraw();
                    }
                }
            }

            WindowEvent::Resized(size) => {
                backend.resize(size.width, size.height);
            }

            WindowEvent::KeyboardInput { event, .. } => {
                if let PhysicalKey::Code(code) = event.physical_key {
                    if let Some(key) = map_key(code) {
                        match event.state {
                            ElementState::Pressed => self.input.press_key(key),
                            ElementState::Released => self.input.release_key(key),
                        }
                    }
                    if self.config.exit_on_escape
                        && code == KeyCode::Escape
                        && event.state == ElementState::Pressed
                    {
                        event_loop.exit();
                    }
                }
            }

            WindowEvent::MouseInput { state, button, .. } => {
                if let Some(b) = map_mouse(button) {
                    match state {
                        ElementState::Pressed => self.input.press_mouse(b),
                        ElementState::Released => self.input.release_mouse(b),
                    }
                }
            }

            WindowEvent::CursorMoved { position, .. } => {
                self.input.mouse_position = Vec2::new(position.x as f32, position.y as f32);
            }

            WindowEvent::MouseWheel { delta, .. } => {
                self.input.scroll_delta += match delta {
                    MouseScrollDelta::LineDelta(_, y) => y,
                    MouseScrollDelta::PixelDelta(p) => p.y as f32,
                };
            }

            WindowEvent::RedrawRequested => {
                let time = self.clock.tick();
                let dt = time.delta_seconds();

                // 1. events in: publish this frame's input snapshot and the
                //    clock as data (GameTime is Copy; scripts read dt here)
                self.world.insert_resource(self.input.clone());
                self.world.insert_resource(time);
                #[cfg(feature = "gamepad")]
                self.world.insert_resource(self.gamepad.poll().clone());

                // 2a. modules first, so the frame's data is settled before
                //     game logic reads it. Modules are peers of systems.
                for m in &mut self.modules {
                    m.update(&mut self.world, dt);
                }

                // 2b. THE FIXED CLOCK (item 8): pay out every whole
                //     fixed_dt this frame owes, carry the remainder. The
                //     same accumulate-and-carry as Animation::update, now
                //     at scheduler scale. The clamp caps catch-up work
                //     after a long stall (the classic spiral-of-death
                //     guard): better to slow the sim than to freeze it.
                self.accumulator = (self.accumulator + dt).min(0.25);
                while self.accumulator >= self.fixed_dt {
                    self.accumulator -= self.fixed_dt;
                    for system in &self.fixed_systems {
                        system(&mut self.world, self.fixed_dt); // ALWAYS the same step
                    }
                }

                // 2c. THE SCHEDULER. v0.1: the entire scheduler. No magic.
                //    A student can read it, write it, and later replace it
                //    with ordering or parallel passes. Registration order
                //    is execution order.
                for system in &self.systems {
                    system(&mut self.world, dt);
                }

                // 3. play back what render systems recorded, through the seams
                if let Some(l) = self.world.try_resource::<DirectionalLight>() {
                    backend.set_light(l.direction, l.color);
                }
                let clear = crate::CORNFLOWER_BLUE;
                if let Err(e) = backend.begin_frame(clear) {
                    log::error!("begin_frame error: {e}");
                }
                let frame = std::mem::take(&mut self.world.resource_mut::<RenderQueue>().sprites);
                for (sprite, camera) in &frame {
                    // camera per call: the per-eye VR seam stays intact
                    if let Err(e) = backend.draw(sprite, camera) {
                        log::error!("draw error: {e}");
                    }
                }
                if let Err(e) = backend.end_frame() {
                    log::error!("end_frame error: {e}"); // present lives HERE only
                }

                // 4. reset input edges for next frame
                self.input.begin_frame();
            }

            _ => {}
        }
    }

    fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
        if self.occluded {
            return; // same occluded fix: no redraw requests while hidden
        }
        if let Some(window) = &self.window {
            window.request_redraw();
        }
    }
}

fn map_mouse(b: winit::event::MouseButton) -> Option<MouseButton> {
    use winit::event::MouseButton as W;
    match b {
        W::Left => Some(MouseButton::Left),
        W::Right => Some(MouseButton::Right),
        W::Middle => Some(MouseButton::Middle),
        _ => None,
    }
}

fn map_key(code: KeyCode) -> Option<Key> {
    use KeyCode::*;
    Some(match code {
        KeyA => Key::A, KeyB => Key::B, KeyC => Key::C, KeyD => Key::D,
        KeyE => Key::E, KeyF => Key::F, KeyG => Key::G, KeyH => Key::H,
        KeyI => Key::I, KeyJ => Key::J, KeyK => Key::K, KeyL => Key::L,
        KeyM => Key::M, KeyN => Key::N, KeyO => Key::O, KeyP => Key::P,
        KeyQ => Key::Q, KeyR => Key::R, KeyS => Key::S, KeyT => Key::T,
        KeyU => Key::U, KeyV => Key::V, KeyW => Key::W, KeyX => Key::X,
        KeyY => Key::Y, KeyZ => Key::Z,
        Digit0 => Key::Num0, Digit1 => Key::Num1, Digit2 => Key::Num2,
        Digit3 => Key::Num3, Digit4 => Key::Num4, Digit5 => Key::Num5,
        Digit6 => Key::Num6, Digit7 => Key::Num7, Digit8 => Key::Num8,
        Digit9 => Key::Num9,
        ArrowUp => Key::Up, ArrowDown => Key::Down,
        ArrowLeft => Key::Left, ArrowRight => Key::Right,
        Space => Key::Space, Enter => Key::Enter, Escape => Key::Escape,
        Tab => Key::Tab, Backspace => Key::Backspace,
        ShiftLeft => Key::LShift, ShiftRight => Key::RShift,
        ControlLeft => Key::LCtrl, ControlRight => Key::RCtrl,
        AltLeft => Key::LAlt, AltRight => Key::RAlt,
        _ => return None,
    })
}
