//! The winit event-loop runner behind [`run`].

use std::sync::Arc;
use std::time::Instant;

use winit::application::ApplicationHandler;
use winit::dpi::LogicalSize;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::window::{Window, WindowId};

use super::{AppCtx, WindowApp, WindowConfig};
use crate::input::InputState;
use crate::render::RenderContext;

/// Opens a window per `config` and runs `app` until it requests exit or the
/// window is closed.
///
/// Blocks the calling thread for the lifetime of the window (an OS
/// requirement: the event loop must run on the main thread).
pub fn run<A: WindowApp>(config: WindowConfig, app: A) -> anyhow::Result<()> {
    let event_loop = EventLoop::new()?;
    // Poll: render continuously (a game loop), rather than waiting for
    // input events like a desktop utility would.
    event_loop.set_control_flow(ControlFlow::Poll);
    let mut runner = Runner {
        config,
        app,
        state: None,
        input: InputState::new(),
        last_frame: None,
        exit: false,
        error: None,
    };
    event_loop.run_app(&mut runner)?;
    match runner.error {
        Some(err) => Err(err),
        None => Ok(()),
    }
}

struct WindowState {
    window: Arc<Window>,
    render: RenderContext,
}

struct Runner<A: WindowApp> {
    config: WindowConfig,
    app: A,
    state: Option<WindowState>,
    /// Accumulated keyboard/mouse/scroll state across the current frame;
    /// pumped from every window event and cleared after `update` returns.
    input: InputState,
    last_frame: Option<Instant>,
    exit: bool,
    /// Initialization/render errors are stashed here and returned from
    /// [`run`], since winit callbacks cannot propagate `Result`.
    error: Option<anyhow::Error>,
}

impl<A: WindowApp> Runner<A> {
    fn create_window(&mut self, event_loop: &ActiveEventLoop) -> anyhow::Result<()> {
        let attrs = Window::default_attributes()
            .with_title(&self.config.title)
            .with_inner_size(LogicalSize::new(self.config.width, self.config.height))
            .with_resizable(self.config.resizable);
        let window = Arc::new(event_loop.create_window(attrs)?);
        let mut render = RenderContext::new(window.clone())?;
        render.set_clear_color(self.config.clear_color);

        let mut state = WindowState { window, render };
        let mut ctx = AppCtx {
            render: &mut state.render,
            window: &state.window,
            exit: &mut self.exit,
            input: &self.input,
            dt: 0.0,
        };
        self.app.init(&mut ctx);
        self.state = Some(state);
        Ok(())
    }

    fn fail(&mut self, event_loop: &ActiveEventLoop, err: anyhow::Error) {
        self.error = Some(err);
        event_loop.exit();
    }
}

impl<A: WindowApp> ApplicationHandler for Runner<A> {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        // On desktop `resumed` fires once at startup; the suspend/resume
        // cycle only matters on mobile, which Oxide does not target yet.
        if self.state.is_none() {
            if let Err(err) = self.create_window(event_loop) {
                self.fail(event_loop, err);
            }
        }
    }

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

        // Fold the event into the per-frame input snapshot before any callback
        // sees it, so `ctx.input()` is always up-to-date for the receiver.
        // `handle_event` is a no-op for non-input events (resize, redraw, …).
        self.input.handle_event(&event);

        // Engine-level handling first…
        match &event {
            WindowEvent::CloseRequested => self.exit = true,
            WindowEvent::Resized(size) => state.render.resize(size.width, size.height),
            WindowEvent::RedrawRequested => {
                let now = Instant::now();
                let dt = self
                    .last_frame
                    .map_or(0.0, |last| (now - last).as_secs_f32());
                self.last_frame = Some(now);

                {
                    let mut ctx = AppCtx {
                        render: &mut state.render,
                        window: &state.window,
                        exit: &mut self.exit,
                        input: &self.input,
                        dt,
                    };
                    self.app.update(&mut ctx);
                }
                // Render the frame, letting the app draw its own passes (e.g.
                // editor UI) into the cleared surface via `App::render`.
                let app = &mut self.app;
                let result = state
                    .render
                    .render_frame_with(&state.window, |rcx| app.render(rcx));
                if let Err(err) = result {
                    self.fail(event_loop, err.into());
                    return;
                }

                // Roll edges/deltas off so the next frame starts clean.
                // Held state and cursor anchor persist by design.
                self.input.end_frame();
            }
            _ => {}
        }

        // …then forward every event raw to the app (including the ones
        // handled above, so apps can observe resizes, close requests, etc.).
        if !matches!(event, WindowEvent::RedrawRequested) {
            let mut ctx = AppCtx {
                render: &mut state.render,
                window: &state.window,
                exit: &mut self.exit,
                input: &self.input,
                dt: 0.0,
            };
            self.app.event(&mut ctx, &event);
        }

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

    fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
        // Continuous rendering: request the next frame as soon as the event
        // queue drains.
        if let Some(state) = &self.state {
            state.window.request_redraw();
        }
    }
}
