//! winit `ApplicationHandler` that drives the xenia-rs UI window.
//!
//! Threading model:
//!
//! ```text
//! ┌─ main thread ────────────────────────────────────────────────┐
//! │ App: ApplicationHandler<SwapEvent>                           │
//! │   on Resumed → create Window (Arc) + RenderState             │
//! │   on UserEvent(SwapEvent) → request redraw                    │
//! │   on WindowEvent::RedrawRequested:                            │
//! │     1. poll gilrs → update AtomicCell<GamepadState>           │
//! │     2. pull latest frontbuffer snapshot from shared Mutex     │
//! │        and upload to wgpu texture                              │
//! │     3. rebuild HUD quads                                       │
//! │     4. render + present                                        │
//! │   on WindowEvent::CloseRequested → flip shutdown, exit        │
//! └──────────────────────────────────────────────────────────────┘
//!
//! ┌─ CPU thread (xenia-app owned) ───────────────────────────────┐
//! │ interpreter loop; VdSwap posts SwapEvent via EventLoopProxy  │
//! └──────────────────────────────────────────────────────────────┘
//! ```

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

use winit::application::ApplicationHandler;
use winit::event::{StartCause, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::window::{WindowAttributes, WindowId};

use crate::bridge::{FrontbufferSnapshot, SwapEvent, UiHandles};
use crate::input::HostInput;
use crate::render::{RenderState, HudVertex, push_string};

struct App {
    handles: Arc<UiHandles>,
    input: HostInput,
    render: Option<RenderState>,
    last_revision: u64,
    title: String,
    start: Instant,
    last_poll: Instant,
    /// Counter of UserEvents received (== VdSwap count).
    swap_events: u64,
    last_swap_info: Option<xenia_kernel::SwapInfo>,
    /// Last `SwapInfo.frame_index` we cleared the frontbuffer for via the
    /// Xenos pipeline. Reset-on-advance triggers a fresh clear for the
    /// next batch of dispatched draws.
    last_xenos_swap_frame: u64,
    /// Shader-blob keys we've already run through the static
    /// `shader_metrics::emit_for` analyzer. Prevents per-frame
    /// double-counting of `gpu.shader.interpret` / `gpu.shader.reject`.
    seen_shader_blobs: std::collections::HashSet<(u8, u32)>,
}

impl App {
    fn new(handles: Arc<UiHandles>, title: String) -> anyhow::Result<Self> {
        let input = HostInput::new()?;
        let now = Instant::now();
        Ok(Self {
            handles,
            input,
            render: None,
            last_revision: 0,
            title,
            start: now,
            last_poll: now,
            swap_events: 0,
            last_swap_info: None,
            last_xenos_swap_frame: 0,
            seen_shader_blobs: std::collections::HashSet::new(),
        })
    }

    fn poll_input(
        input: &mut HostInput,
        handles: &UiHandles,
    ) {
        input.poll();
        let connected = input.is_connected();
        handles.gamepad_connected.store(connected, Ordering::Relaxed);
        let snap = input.snapshot();
        handles.gamepad.store(snap);
    }

    fn build_hud(
        handles: &UiHandles,
        title: &str,
        start: Instant,
        swap_events: u64,
        last_swap_info: Option<xenia_kernel::SwapInfo>,
        rs: &RenderState,
    ) -> Vec<HudVertex> {
        let mut verts: Vec<HudVertex> = Vec::with_capacity(512);
        let (sw, _sh) = rs.surface_size();
        let scale = ((sw as f32 / 960.0).max(1.0)).min(3.0);
        // Glyph cell is 8 px tall at scale=1; `line_h` = row pitch. 14 px
        // gives ~6 px of breathing room between lines so adjacent text never
        // overlaps (previous bug: line positions `1.9×` and `2.0×` were
        // within one glyph of each other and smeared together).
        let line_h = 14.0 * scale;
        let pad = 6.0 * scale;
        let white = [1.0, 1.0, 1.0, 0.95];
        let green = [0.55, 0.95, 0.55, 0.95];
        let amber = [0.95, 0.85, 0.4, 0.95];
        let cyan = [0.55, 0.85, 0.95, 0.95];
        let muted = [0.75, 0.75, 0.85, 0.85];

        let uptime = start.elapsed().as_secs_f32();
        let pad_snapshot = handles.gamepad.load();
        let connected = handles.gamepad_connected.load(Ordering::Relaxed);

        // Row 0: title + uptime + live instruction rate.
        let mut y = pad;
        let ins_total = handles
            .instructions_counter
            .load(Ordering::Relaxed);
        let ips = if uptime > 0.05 {
            ins_total as f32 / uptime
        } else {
            0.0
        };
        push_string(
            &mut verts,
            &format!(
                "xenia-rs UI   {}   uptime {:>6.1}s   ins {:>10}  ({:>5.1} kIPS)",
                title,
                uptime,
                ins_total,
                ips / 1000.0
            ),
            pad,
            y,
            scale,
            green,
        );

        // Row 1: swap info.
        y += line_h;
        let swap_line = match last_swap_info {
            Some(info) => format!(
                "Swaps: {:>5}  fb {:#010x}  {}x{}  fmt {}  cs {}",
                swap_events, info.frontbuffer_addr, info.width, info.height,
                info.texture_format, info.color_space
            ),
            None => format!("Swaps: {:>5}  (waiting for first VdSwap)", swap_events),
        };
        push_string(&mut verts, &swap_line, pad, y, scale, muted);

        // Row 2: GPU totals.
        y += line_h;
        let gpu_line = match last_swap_info {
            Some(info) => format!(
                "GPU:   draws={:>6}  pkts={:>8}  IBs={:>4}  waits={:>4}  resolves={:>4} (cp={:>4}/sk={:>3})  RTs={:>3}  irq={:>4}/{:<4}  xlated={:>3}",
                info.draws_total,
                info.packets_total,
                info.indirect_buffer_jumps,
                info.wait_reg_mem_blocks,
                info.resolves_total,
                info.resolves_copied_total,
                info.resolves_skipped_total,
                info.unique_render_targets,
                info.interrupts_delivered,
                info.interrupts_dropped,
                rs.translated_pipeline_count(),
            ),
            None => String::from(
                "GPU:   draws=     0  pkts=        0  IBs=   0  waits=   0  resolves=   0 (cp=   0/sk=  0)  RTs=  0  irq=   0/0    xlated=  0",
            ),
        };
        push_string(&mut verts, &gpu_line, pad, y, scale, muted);

        // Row 2b: First-Pixels M4 — rendering-path observability. Shows
        // whether the Xenos pipeline is actually dispatching and whether
        // the translator or the interpreter served each draw. A `1x1`
        // fb_size is the classic signature of "ensure_frontbuffer_at_least
        // never fired" — useful at-a-glance diagnostic when nothing
        // visibly renders.
        y += line_h;
        let (fbw, fbh) = rs.frontbuffer_size();
        let render_line = format!(
            "Render: xdispatch: xlated={:>5} interp={:>5}  xlated-pipelines={:>3}  real-geo={:>5}  tex-cache={:>3}  fb={}x{}",
            rs.xenos_dispatches_translator,
            rs.xenos_dispatches_interpreter,
            rs.translated_pipeline_count(),
            rs.real_geometry_draws(),
            rs.host_texture_count(),
            fbw,
            fbh,
        );
        push_string(&mut verts, &render_line, pad, y, scale, cyan);

        // Row 3: latest draw details.
        y += line_h;
        let draw_line = match last_swap_info {
            Some(info) if info.draws_total > 0 => {
                let prim = match info.last_draw_prim {
                    0 => "None",
                    1 => "PointList",
                    2 => "LineList",
                    3 => "LineStrip",
                    4 => "TriangleList",
                    5 => "TriangleFan",
                    6 => "TriangleStrip",
                    8 => "RectangleList",
                    13 => "QuadList",
                    _ => "Unknown",
                };
                format!(
                    "Draw:  last {} verts={}  code={:#x}",
                    prim, info.last_draw_vertex_count, info.last_draw_prim
                )
            }
            _ => String::from("Draw:  (no DRAW_INDX seen yet)"),
        };
        push_string(&mut verts, &draw_line, pad, y, scale, cyan);

        // Row 4: gamepad status.
        y += line_h;
        let pad_line = if connected {
            format!(
                "Pad[0]: btn {:04X}  LT {:02X}  RT {:02X}  L({:>+6},{:>+6})  R({:>+6},{:>+6})",
                pad_snapshot.buttons,
                pad_snapshot.left_trigger,
                pad_snapshot.right_trigger,
                pad_snapshot.left_stick_x,
                pad_snapshot.left_stick_y,
                pad_snapshot.right_stick_x,
                pad_snapshot.right_stick_y,
            )
        } else {
            String::from("Pad[0]: (no controller)")
        };
        push_string(
            &mut verts,
            &pad_line,
            pad,
            y,
            scale,
            if connected { white } else { amber },
        );

        // Row 5 (blank gap) + help text.
        y += line_h * 2.0;
        let help = "Close window or press Alt+F4 to stop the CPU thread and exit.";
        push_string(&mut verts, help, pad, y, scale, muted);

        verts
    }

    fn ingest_frontbuffer(
        handles: &UiHandles,
        last_revision: &mut u64,
        last_swap_info: &mut Option<xenia_kernel::SwapInfo>,
        rs: &mut RenderState,
    ) {
        let Ok(snap) = handles.frontbuffer.lock() else { return };
        if snap.revision == *last_revision {
            return;
        }
        *last_revision = snap.revision;
        *last_swap_info = snap.info;
        let FrontbufferSnapshot {
            width,
            height,
            ref rgba,
            ..
        } = *snap;
        if width == 0 || height == 0 || rgba.is_empty() {
            return;
        }
        rs.upload_frontbuffer(width, height, rgba);
    }
}

impl ApplicationHandler<SwapEvent> for App {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        if self.render.is_some() {
            return;
        }
        let attrs = WindowAttributes::default()
            .with_title(format!("xenia-rs — {}", self.title))
            .with_inner_size(winit::dpi::LogicalSize::new(1280.0, 720.0));
        let window = match event_loop.create_window(attrs) {
            Ok(w) => Arc::new(w),
            Err(e) => {
                tracing::error!("failed to create window: {e}");
                event_loop.exit();
                return;
            }
        };
        match RenderState::new(window.clone()) {
            Ok(rs) => self.render = Some(rs),
            Err(e) => {
                tracing::error!("failed to initialize wgpu: {e}");
                event_loop.exit();
            }
        }
    }

    fn new_events(&mut self, event_loop: &ActiveEventLoop, _cause: StartCause) {
        // Check shutdown (e.g. CPU thread crashed and flipped the flag).
        if self.handles.shutdown.load(Ordering::Relaxed) {
            event_loop.exit();
            return;
        }
        // Animate the HUD/gamepad readout at ~60 Hz even without swap events.
        let now = Instant::now();
        if now.duration_since(self.last_poll).as_millis() >= 16 {
            self.last_poll = now;
            Self::poll_input(&mut self.input, &self.handles);
            if let Some(ref rs) = self.render {
                rs.window.request_redraw();
            }
        }
        event_loop.set_control_flow(ControlFlow::Poll);
    }

    fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: SwapEvent) {
        self.swap_events = self.swap_events.wrapping_add(1);
        self.last_swap_info = Some(event.0);
        if let Some(ref rs) = self.render {
            rs.window.request_redraw();
        }
    }

    #[tracing::instrument(skip_all, name = "ui.window_event")]
    fn window_event(
        &mut self,
        event_loop: &ActiveEventLoop,
        _window_id: WindowId,
        event: WindowEvent,
    ) {
        if self.render.is_none() {
            return;
        }
        match event {
            WindowEvent::CloseRequested => {
                self.handles.request_shutdown();
                event_loop.exit();
            }
            WindowEvent::Resized(size) => {
                if let Some(rs) = self.render.as_mut() {
                    rs.resize(size.width, size.height);
                }
            }
            WindowEvent::RedrawRequested => {
                let _redraw = tracing::debug_span!("ui.redraw").entered();
                Self::poll_input(&mut self.input, &self.handles);
                let rs = self.render.as_mut().unwrap();

                // P3 draw-dispatch: if the GPU has captured more draws than
                // we've rendered via the Xenos host pipeline, catch up.
                // We prefer the Xenos pipeline over the raw frontbuffer
                // scrape once any draw has fired — the scrape is only
                // useful while the game is pre-draw (no DRAW_INDX yet).
                let draws_total = self
                    .last_swap_info
                    .map(|s| s.draws_total)
                    .unwrap_or(0);
                if draws_total > 0 {
                    rs.ensure_frontbuffer_at_least(1280, 720);
                    let already = rs.xenos_draws_rendered;
                    if draws_total > already {
                        let frame_idx = self
                            .last_swap_info
                            .map(|s| s.frame_index)
                            .unwrap_or(0);
                        if frame_idx != self.last_xenos_swap_frame {
                            // iterate-3AE: clear to BLACK, matching canary's
                            // splash background. The old navy `[0.04,0.04,0.06]`
                            // was an iterate-3S debug placeholder never matched
                            // to the guest. The splash background-fill draw is a
                            // full-screen Xbox-360 RectangleList (3 verts → a HW
                            // rectangle covering the whole screen); the UI replay
                            // draws it as a single triangle (the 4th implied
                            // corner isn't synthesized), so only the diagonal
                            // half is covered. With a navy clear the uncovered
                            // half showed a navy diagonal in the brief
                            // pre/inter-logo transition frames (where that fill
                            // is the only coverage). Canary's background there is
                            // black, and the guest's fill itself resolves to
                            // black, so a black clear makes the uncovered half
                            // match — the transition is uniformly black like the
                            // oracle. (Full RectangleList→rectangle expansion is
                            // the deeper fix and a separate follow-up; under a
                            // black clear the half-coverage is invisible.)
                            rs.clear_frontbuffer([0.0, 0.0, 0.0, 1.0]);
                            self.last_xenos_swap_frame = frame_idx;
                        }
                        let delta = (draws_total - already) as u32;
                        let constants = self
                            .handles
                            .xenos_constants
                            .lock()
                            .map(|g| *g)
                            .unwrap_or_default();
                        // P5: bind any kernel-decoded primary texture
                        // before dispatching the draw. `None` reverts the
                        // pipeline's magenta-stub placeholder.
                        let tex_payload = self
                            .handles
                            .primary_texture
                            .lock()
                            .ok()
                            .and_then(|g| g.clone());
                        rs.bind_primary_texture(tex_payload);

                        // iterate-3O real-render slice: prefer replaying the
                        // *real* captured guest geometry. The kernel publishes
                        // one `DrawCapture` per `PM4_DRAW_INDX*` this frame
                        // (real vertices + prim type + shader keys). Fall back
                        // to the legacy synthetic dispatch only when no capture
                        // is available (e.g. capture disabled), so we never
                        // regress to a blank screen.
                        let captures: Vec<xenia_gpu::draw_capture::DrawCapture> = self
                            .handles
                            .geometry
                            .lock()
                            .map(|g| g.clone())
                            .unwrap_or_default();
                        let blobs: std::collections::HashMap<u32, Vec<u32>> = self
                            .handles
                            .shader_blobs
                            .lock()
                            .map(|g| g.clone())
                            .unwrap_or_default();
                        if !captures.is_empty() {
                            rs.dispatch_xenos_captures(
                                &captures,
                                &blobs,
                                &constants,
                                &mut self.seen_shader_blobs,
                            );
                        } else {
                            // Legacy synthetic-geometry fallback (placeholder).
                            let (verts_hint, prim_kind, vs_key, ps_key) = self
                                .last_swap_info
                                .map(|s| {
                                    (
                                        s.last_draw_vertex_count.max(3),
                                        s.last_draw_prim,
                                        s.vs_blob_key,
                                        s.ps_blob_key,
                                    )
                                })
                                .unwrap_or((3, 4, 0, 0));
                            let raw_vs = blobs.get(&vs_key).cloned().unwrap_or_default();
                            let raw_ps = blobs.get(&ps_key).cloned().unwrap_or_default();
                            let parsed_vs = xenia_gpu::ucode::parse_shader(&raw_vs);
                            let parsed_ps = xenia_gpu::ucode::parse_shader(&raw_ps);
                            if self.seen_shader_blobs.insert((0u8, vs_key)) {
                                xenia_gpu::shader_metrics::emit_for(&parsed_vs, "vs");
                            }
                            if self.seen_shader_blobs.insert((1u8, ps_key)) {
                                xenia_gpu::shader_metrics::emit_for(&parsed_ps, "ps");
                            }
                            let vs_packed = xenia_gpu::ucode::pack_for_wgsl(&parsed_vs);
                            let ps_packed = xenia_gpu::ucode::pack_for_wgsl(&parsed_ps);
                            rs.dispatch_xenos_draws(
                                already,
                                delta,
                                verts_hint,
                                prim_kind,
                                vs_key,
                                ps_key,
                                &parsed_vs,
                                &parsed_ps,
                                &vs_packed,
                                &ps_packed,
                                &constants,
                            );
                        }
                    }
                } else {
                    Self::ingest_frontbuffer(
                        &self.handles,
                        &mut self.last_revision,
                        &mut self.last_swap_info,
                        rs,
                    );
                }
                let verts = Self::build_hud(
                    &self.handles,
                    &self.title,
                    self.start,
                    self.swap_events,
                    self.last_swap_info,
                    rs,
                );
                rs.set_hud_vertices(&verts);
                match rs.render() {
                    Ok(()) => {}
                    Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
                        let (w, h) = rs.surface_size();
                        rs.resize(w, h);
                    }
                    Err(wgpu::SurfaceError::OutOfMemory) => {
                        tracing::error!("wgpu surface OOM — exiting");
                        self.handles.request_shutdown();
                        event_loop.exit();
                    }
                    Err(e) => {
                        tracing::warn!("wgpu surface error: {e:?}");
                    }
                }
            }
            _ => {}
        }
    }
}

/// Run the UI on the calling thread (must be the main thread on macOS/Windows).
///
/// Takes ownership of the [`EventLoop`] (created by the caller so the caller
/// can mint the `EventLoopProxy` used by the kernel bridge). Returns when the
/// window is closed or the CPU thread flipped the shutdown flag.
pub fn run(
    event_loop: EventLoop<SwapEvent>,
    handles: UiHandles,
    title: impl Into<String>,
) -> anyhow::Result<()> {
    let mut app = App::new(Arc::new(handles), title.into())?;
    event_loop.run_app(&mut app)?;
    // Make sure the CPU thread sees shutdown even on graceful exit.
    app.handles.request_shutdown();
    Ok(())
}
