//! Generic windowed ratatui backend using winit + wgpu.
//!
//! This crate provides a GPU-accelerated native window that hosts any
//! application implementing the [`GuiApplication`] trait.  All winit input
//! events are translated to crossterm types so consumers can reuse the same
//! input handling as a terminal-based frontend.

#[cfg(target_os = "macos")]
pub mod macos;
mod native_menu;
pub mod platform;

use std::collections::HashMap;
use std::num::NonZeroU32;
use std::sync::Arc;
use std::time::{Duration, Instant};

use anyhow::{Context, Result as AnyhowResult};
use crossterm::event::{
    KeyCode, KeyEvent as CtKeyEvent, KeyEventKind, KeyEventState, KeyModifiers, MediaKeyCode,
    ModifierKeyCode, MouseButton as CtMouseButton, MouseEvent as CtMouseEvent, MouseEventKind,
};
use fresh_core::menu::{Menu, MenuContext};
use ratatui::backend::Backend;
use ratatui::style::Color;
use ratatui::Terminal;
use ratatui_wgpu::{Builder, Dimensions, Font, WgpuBackend};
use winit::application::ApplicationHandler;
use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::keyboard::KeyLocation;
use winit::keyboard::{Key, NamedKey};
use winit::window::{Window, WindowAttributes, WindowId};

use native_menu::NativeMenuBar;

/// Embedded JetBrains Mono Regular font (SIL Open Font License 1.1).
const FONT_DATA: &[u8] = include_bytes!("../fonts/JetBrainsMono-Regular.ttf");

/// Cross-platform fallback application icon (256×256 RGBA PNG).
///
/// Used as the winit `Window::set_window_icon` value, which Linux WMs read
/// as `_NET_WM_ICON` and scale down per task-bar size.  On Windows we
/// additionally override this immediately after window creation with the
/// multi-size HICON from the .exe's embedded `RT_GROUP_ICON` resource — see
/// [`platform::set_window_icon`].  256 px scales down well; 32 px upscales
/// poorly to 48/64 at 150%/200% DPI.
const ICON_PNG: &[u8] = include_bytes!("../resources/icon_256x256.png");

/// Frame duration target (60fps).
const FRAME_DURATION: Duration = Duration::from_millis(16);

// ---------------------------------------------------------------------------
// Public trait — implemented by the consuming application
// ---------------------------------------------------------------------------

/// Trait for applications that can be hosted in the GUI window.
///
/// All input events are delivered as crossterm types so the application can
/// share input-handling logic with a terminal frontend.
///
/// The trait also defines the application's menu structure via
/// [`menu_definitions`](Self::menu_definitions) and
/// [`take_menu_update`](Self::take_menu_update).  The GUI layer uses these to
/// build and maintain platform-native menus (e.g. the macOS menu bar) without
/// the application needing to know which platform it is running on.
pub trait GuiApplication {
    /// Handle a translated key event.
    fn on_key(&mut self, key: CtKeyEvent) -> AnyhowResult<()>;

    /// Handle a mouse event.  Returns `true` if a re-render is needed.
    fn on_mouse(&mut self, mouse: CtMouseEvent) -> AnyhowResult<bool>;

    /// Render the application into a ratatui frame.
    fn render(&mut self, frame: &mut ratatui::Frame);

    /// Per-tick housekeeping (called every frame).
    /// Returns `true` if a re-render is needed.
    fn tick(&mut self) -> AnyhowResult<bool>;

    /// Whether the application wants to quit.
    fn should_quit(&self) -> bool;

    /// Handle terminal resize to `(cols, rows)`.
    fn resize(&mut self, cols: u16, rows: u16);

    /// Called when the window is about to close (e.g. save state).
    fn on_close(&mut self);

    /// Return the application's initial menu bar definition.
    ///
    /// Called once during initialization to build platform-native menus.
    /// The returned [`Menu`] items are the same model used by the editor's
    /// built-in TUI menu bar — single source of truth.
    ///
    /// **Important:** `DynamicSubmenu` items should be expanded (resolved to
    /// `Submenu`) before returning so that the native menu layer can render
    /// them.
    ///
    /// Default: empty (no native menus).
    fn menu_definitions(&self) -> Vec<Menu> {
        Vec::new()
    }

    /// Return an updated menu bar definition if the menus have changed since
    /// the last call.
    ///
    /// The GUI event loop calls this every frame. Return `Some(menus)` to
    /// trigger a native menu rebuild, or `None` if nothing changed.
    ///
    /// Default: always returns `None`.
    fn take_menu_update(&mut self) -> Option<Vec<Menu>> {
        None
    }

    /// Return the current menu context (boolean state flags).
    ///
    /// The GUI layer uses these values to update enabled/disabled state and
    /// checkmark display on native menu items each frame.  The context is
    /// the same `MenuContext` the editor uses for its TUI menu bar.
    ///
    /// Default: empty context (all flags false).
    fn menu_context(&self) -> MenuContext {
        MenuContext::default()
    }

    /// Handle a menu action triggered by the native platform menu bar.
    ///
    /// `action` and `args` come directly from the
    /// [`MenuItem::Action`](fresh_core::menu::MenuItem::Action) that the user
    /// clicked in the native menu.
    ///
    /// Default implementation does nothing.
    fn on_menu_action(&mut self, _action: &str, _args: &HashMap<String, serde_json::Value>) {}

    /// Return an updated ANSI color table if the theme changed since the
    /// last call.
    ///
    /// The GUI event loop polls this every frame. Return `Some(table)` to
    /// update the wgpu backend's color table, or `None` if unchanged.
    ///
    /// Default: always returns `None`.
    fn take_color_update(&mut self) -> Option<ColorTable> {
        None
    }
}

// ---------------------------------------------------------------------------
// Window configuration
// ---------------------------------------------------------------------------

/// Configuration for the GUI window.
pub struct GuiConfig {
    /// Window title.
    pub title: String,
    /// Initial window width in pixels.
    pub width: u32,
    /// Initial window height in pixels.
    pub height: u32,
    /// Default background color used when a cell has `Color::Reset`.
    /// Defaults to `Color::White` (ratatui-wgpu default).
    pub reset_bg: Color,
    /// Default foreground color used when a cell has `Color::Reset`.
    /// Defaults to `Color::Black` (ratatui-wgpu default).
    pub reset_fg: Color,
    /// ANSI base-16 color table. When `None`, uses ratatui-wgpu defaults.
    pub color_table: Option<ColorTable>,
}

impl Default for GuiConfig {
    fn default() -> Self {
        Self {
            title: "Fresh".into(),
            width: 1280,
            height: 800,
            reset_bg: Color::White,
            reset_fg: Color::Black,
            color_table: None,
        }
    }
}

/// ANSI color table tuned for dark backgrounds (bg ~#1e1e1e).
///
/// Standard colors are brightened and light variants are vivid to ensure
/// readability against dark editor backgrounds.
// Re-export so consumers (e.g. fresh-editor/gui) can name the type
// returned by `GuiApplication::take_color_update`.
pub use ratatui_wgpu::ColorTable;

pub fn dark_color_table() -> ColorTable {
    ColorTable {
        BLACK: [0, 0, 0],
        RED: [204, 60, 60],
        GREEN: [80, 180, 80],
        YELLOW: [220, 180, 60],
        BLUE: [70, 130, 230],
        MAGENTA: [190, 90, 220],
        CYAN: [60, 190, 190],
        GRAY: [160, 160, 160],
        DARKGRAY: [100, 100, 100],
        LIGHTRED: [240, 110, 110],
        LIGHTGREEN: [130, 220, 130],
        LIGHTYELLOW: [240, 220, 130],
        LIGHTBLUE: [130, 170, 255],
        LIGHTMAGENTA: [220, 140, 255],
        LIGHTCYAN: [120, 230, 230],
        WHITE: [230, 230, 230],
    }
}

/// ANSI color table tuned for light backgrounds (bg ~#ffffff).
///
/// Colors are darkened / more saturated so they remain readable against
/// white or very light editor backgrounds.
pub fn light_color_table() -> ColorTable {
    ColorTable {
        BLACK: [0, 0, 0],
        RED: [180, 0, 0],
        GREEN: [0, 130, 0],
        YELLOW: [150, 120, 0],
        BLUE: [0, 50, 180],
        MAGENTA: [140, 0, 140],
        CYAN: [0, 130, 130],
        GRAY: [130, 130, 130],
        DARKGRAY: [80, 80, 80],
        LIGHTRED: [210, 60, 60],
        LIGHTGREEN: [40, 160, 40],
        LIGHTYELLOW: [180, 150, 0],
        LIGHTBLUE: [50, 90, 210],
        LIGHTMAGENTA: [170, 50, 170],
        LIGHTCYAN: [0, 160, 160],
        WHITE: [255, 255, 255],
    }
}

// ---------------------------------------------------------------------------
// Public entry point
// ---------------------------------------------------------------------------

/// Run the GUI event loop.
///
/// `create_app` is called once the window and wgpu backend are ready, with
/// the initial terminal grid dimensions `(cols, rows)`.
pub fn run<F, A>(config: GuiConfig, create_app: F) -> AnyhowResult<()>
where
    F: FnOnce(u16, u16) -> AnyhowResult<A> + 'static,
    A: GuiApplication + 'static,
{
    // Per-platform process-level setup (e.g. Windows AppUserModelID).
    // Must happen before the EventLoop is constructed.
    platform::init();

    let event_loop = EventLoop::new().context("Failed to create winit event loop")?;
    // Use WaitUntil for frame pacing instead of Poll.  Poll causes winit to
    // schedule a CFRunLoopTimer at f64::MIN (fire immediately), which
    // continuously wakes the run loop — including during macOS's modal menu
    // tracking loop, causing the highlighted menu to jump to the leftmost
    // item.  WaitUntil achieves the same ~60fps without aggressive polling
    // and is also friendlier to CPU / battery.
    event_loop.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
        Instant::now() + FRAME_DURATION,
    ));

    let mut runner: WgpuRunner<A> = WgpuRunner {
        config,
        create_app: Some(Box::new(create_app)),
        state: None,
    };

    event_loop
        .run_app(&mut runner)
        .context("winit event loop error")?;

    Ok(())
}

// ---------------------------------------------------------------------------
// Internal runner
// ---------------------------------------------------------------------------

type CreateAppFn<A> = Box<dyn FnOnce(u16, u16) -> AnyhowResult<A>>;

/// Winit application that bridges winit/wgpu to the [`GuiApplication`] trait.
struct WgpuRunner<A: GuiApplication> {
    config: GuiConfig,
    /// Factory called in `resumed()` once the window is ready.
    create_app: Option<CreateAppFn<A>>,
    /// Runtime state — created in `resumed()`.
    state: Option<RunnerState<A>>,
}

struct RunnerState<A: GuiApplication> {
    app: A,
    terminal: Terminal<WgpuBackend<'static, 'static>>,
    window: Arc<Window>,
    needs_render: bool,
    last_render: Instant,
    /// Current cursor position in pixels (tracked across CursorMoved events).
    cursor_position: (f64, f64),
    /// Current modifier state (tracked across ModifiersChanged events).
    modifiers: KeyModifiers,
    /// Tracks which mouse button is currently held (for drag detection).
    pressed_button: Option<CtMouseButton>,
    /// Cell dimensions in pixels (width, height) for pixel-to-cell conversion.
    cell_size: (f64, f64),
    /// Which Alt/Option key is currently held (for macOS Left/Right distinction).
    alt_location: Option<KeyLocation>,
    /// Platform-native menu bar (macOS: real AppKit menus; other: no-op stub).
    native_menu: NativeMenuBar,
}

impl<A: GuiApplication + 'static> ApplicationHandler for WgpuRunner<A> {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        if self.state.is_some() {
            return; // Already initialized
        }

        match self.create_state(event_loop) {
            Ok(state) => {
                state.window.request_redraw();
                self.state = Some(state);
            }
            Err(e) => {
                tracing::error!("Failed to initialize GUI: {:#}", e);
                event_loop.exit();
            }
        }
    }

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

        match event {
            WindowEvent::CloseRequested => {
                state.app.on_close();
                event_loop.exit();
            }

            WindowEvent::Resized(size) => {
                if size.width > 0 && size.height > 0 {
                    state.terminal.backend_mut().resize(size.width, size.height);
                    // Re-derive cell size from the backend after resize
                    if let Ok(ws) = state.terminal.backend_mut().window_size() {
                        let cols = ws.columns_rows.width;
                        let rows = ws.columns_rows.height;
                        state.cell_size = (
                            ws.pixels.width as f64 / cols.max(1) as f64,
                            ws.pixels.height as f64 / rows.max(1) as f64,
                        );
                        state.app.resize(cols, rows);
                    }
                    state.needs_render = true;
                }
            }

            WindowEvent::ModifiersChanged(mods) => {
                state.modifiers = translate_modifiers(&mods.state());
            }

            WindowEvent::KeyboardInput { event, .. } => {
                // Track Alt key location (before early return for Released)
                if let Key::Named(NamedKey::Alt) = &event.logical_key {
                    match event.state {
                        ElementState::Pressed => {
                            state.alt_location = Some(event.location);
                        }
                        ElementState::Released => {
                            state.alt_location = None;
                        }
                    }
                }

                if event.state == ElementState::Released {
                    return;
                }
                if let Some(key_event) =
                    translate_key_event(&event, state.modifiers, state.alt_location)
                {
                    if let Err(e) = state.app.on_key(key_event) {
                        tracing::error!("Key handling error: {}", e);
                    }
                    state.needs_render = true;
                }
            }

            WindowEvent::MouseInput {
                state: btn_state,
                button,
                ..
            } => {
                if let Some(ct_btn) = translate_mouse_button(button) {
                    let kind = match btn_state {
                        ElementState::Pressed => {
                            state.pressed_button = Some(ct_btn);
                            MouseEventKind::Down(ct_btn)
                        }
                        ElementState::Released => {
                            state.pressed_button = None;
                            MouseEventKind::Up(ct_btn)
                        }
                    };
                    let (col, row) = pixel_to_cell(state.cursor_position, state.cell_size);
                    let mouse_event = CtMouseEvent {
                        kind,
                        column: col,
                        row,
                        modifiers: state.modifiers,
                    };
                    match state.app.on_mouse(mouse_event) {
                        Ok(true) => state.needs_render = true,
                        Ok(false) => {}
                        Err(e) => tracing::error!("Mouse handling error: {}", e),
                    }
                }
            }

            WindowEvent::CursorMoved { position, .. } => {
                state.cursor_position = (position.x, position.y);
                let (col, row) = pixel_to_cell(state.cursor_position, state.cell_size);
                let kind = match state.pressed_button {
                    Some(btn) => MouseEventKind::Drag(btn),
                    None => MouseEventKind::Moved,
                };
                let mouse_event = CtMouseEvent {
                    kind,
                    column: col,
                    row,
                    modifiers: state.modifiers,
                };
                match state.app.on_mouse(mouse_event) {
                    Ok(true) => state.needs_render = true,
                    Ok(false) => {}
                    Err(e) => tracing::error!("Mouse handling error: {}", e),
                }
            }

            WindowEvent::MouseWheel { delta, .. } => {
                let (h_lines, v_lines) = match delta {
                    MouseScrollDelta::LineDelta(h, v) => (h as i32, v as i32),
                    MouseScrollDelta::PixelDelta(pos) => {
                        let line_h = state.cell_size.1.max(1.0);
                        ((pos.x / line_h) as i32, (pos.y / line_h) as i32)
                    }
                };
                let (col, row) = pixel_to_cell(state.cursor_position, state.cell_size);

                // Vertical scroll
                for _ in 0..v_lines.unsigned_abs() {
                    let kind = if v_lines > 0 {
                        MouseEventKind::ScrollUp
                    } else {
                        MouseEventKind::ScrollDown
                    };
                    let mouse_event = CtMouseEvent {
                        kind,
                        column: col,
                        row,
                        modifiers: state.modifiers,
                    };
                    match state.app.on_mouse(mouse_event) {
                        Ok(true) => state.needs_render = true,
                        Ok(false) => {}
                        Err(e) => tracing::error!("Scroll handling error: {}", e),
                    }
                }

                // Horizontal scroll
                for _ in 0..h_lines.unsigned_abs() {
                    let kind = if h_lines > 0 {
                        MouseEventKind::ScrollRight
                    } else {
                        MouseEventKind::ScrollLeft
                    };
                    let mouse_event = CtMouseEvent {
                        kind,
                        column: col,
                        row,
                        modifiers: state.modifiers,
                    };
                    match state.app.on_mouse(mouse_event) {
                        Ok(true) => state.needs_render = true,
                        Ok(false) => {}
                        Err(e) => tracing::error!("Scroll handling error: {}", e),
                    }
                }
            }

            WindowEvent::RedrawRequested => {
                if state.needs_render && state.last_render.elapsed() >= FRAME_DURATION {
                    if let Err(e) = state.terminal.draw(|frame| state.app.render(frame)) {
                        tracing::error!("Render error: {}", e);
                    }
                    state.last_render = Instant::now();
                    state.needs_render = false;
                }
            }

            _ => {}
        }
    }

    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
        let Some(state) = self.state.as_mut() else {
            return;
        };

        // While the macOS menu bar is being tracked (user hovering over menus),
        // switch to pure Wait mode so winit does not schedule any timer wake-
        // ups.  The modal tracking run loop is very sensitive to external
        // interference; even periodic timer wake-ups can cause the highlighted
        // menu to jump to the leftmost item.
        if state.native_menu.is_tracking() {
            event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait);
            return;
        }
        // Schedule the next frame wake-up (~60fps).
        event_loop.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
            Instant::now() + FRAME_DURATION,
        ));

        // Poll native menu bar for user clicks and dispatch to the app.
        if let Some(action) = state.native_menu.poll_action() {
            state.app.on_menu_action(&action.action, &action.args);
            state.needs_render = true;
        }

        match state.app.tick() {
            Ok(true) => state.needs_render = true,
            Ok(false) => {}
            Err(e) => tracing::error!("Tick error: {}", e),
        }

        // If the app signalled a menu model change, rebuild native menus.
        if let Some(updated_menus) = state.app.take_menu_update() {
            let ctx = state.app.menu_context();
            state
                .native_menu
                .update(&updated_menus, &self.config.title, &ctx);
        }

        // If the app signalled a color table change (e.g. theme switch),
        // push it to the wgpu backend so ANSI named colors match the theme.
        if let Some(table) = state.app.take_color_update() {
            state.terminal.backend_mut().update_color_table(table);
            state.needs_render = true;
        }

        // Sync native menu item states (enabled/disabled, checkmarks) from
        // the application's current MenuContext — cheap incremental update.
        let ctx = state.app.menu_context();
        state.native_menu.sync_state(&ctx);

        if state.app.should_quit() {
            state.app.on_close();
            event_loop.exit();
            return;
        }

        if state.needs_render {
            state.window.request_redraw();
        }
    }
}

impl<A: GuiApplication> WgpuRunner<A> {
    fn create_state(&mut self, event_loop: &ActiveEventLoop) -> AnyhowResult<RunnerState<A>> {
        let mut window_attrs = WindowAttributes::default()
            .with_title(&self.config.title)
            .with_inner_size(winit::dpi::PhysicalSize::new(
                self.config.width,
                self.config.height,
            ));

        // Set window icon from embedded PNG (shows in taskbar/dock on supported platforms)
        if let Some(icon) = load_window_icon() {
            window_attrs = window_attrs.with_window_icon(Some(icon));
        }

        let window = Arc::new(
            event_loop
                .create_window(window_attrs)
                .context("Failed to create window")?,
        );

        // On Windows, override the cross-platform PNG icon with the multi-size
        // HICON loaded from the .exe's embedded resource so the running
        // window's icon (small + large) is sharp at every DPI and matches
        // what Explorer / Properties / Alt-Tab show.  No-op elsewhere.
        platform::set_window_icon(&window);

        let size = window.inner_size();

        // Build the wgpu backend (async adapter/device request — block on it).
        let rt = tokio::runtime::Runtime::new().context("Failed to create tokio runtime")?;
        let font = Font::new(FONT_DATA).context("Failed to load embedded font")?;

        let mut builder = Builder::from_font(font)
            .with_width_and_height(Dimensions {
                width: NonZeroU32::new(size.width).unwrap_or(NonZeroU32::new(1).unwrap()),
                height: NonZeroU32::new(size.height).unwrap_or(NonZeroU32::new(1).unwrap()),
            })
            .with_bg_color(self.config.reset_bg)
            .with_fg_color(self.config.reset_fg);
        if let Some(table) = self.config.color_table.clone() {
            builder = builder.with_color_table(table);
        }
        let backend = rt
            .block_on(builder.build_with_target(window.clone()))
            .context("Failed to create wgpu backend")?;

        let mut terminal = Terminal::new(backend).context("Failed to create ratatui terminal")?;

        let win_size = terminal
            .backend_mut()
            .window_size()
            .context("Failed to query window size from backend")?;
        let cols = win_size.columns_rows.width;
        let rows = win_size.columns_rows.height;
        let cell_size = (
            win_size.pixels.width as f64 / cols.max(1) as f64,
            win_size.pixels.height as f64 / rows.max(1) as f64,
        );

        let create_app = self
            .create_app
            .take()
            .context("create_app already consumed")?;
        let app = create_app(cols, rows)?;

        // Build platform-native menu bar from the app's menu model.
        let menus = app.menu_definitions();
        let ctx = app.menu_context();
        let native_menu = NativeMenuBar::build(&menus, &self.config.title, &ctx);

        Ok(RunnerState {
            app,
            terminal,
            window,
            needs_render: true,
            last_render: Instant::now(),
            cursor_position: (0.0, 0.0),
            modifiers: KeyModifiers::NONE,
            pressed_button: None,
            cell_size,
            alt_location: None,
            native_menu,
        })
    }
}

// ---------------------------------------------------------------------------
// Input translation helpers
// ---------------------------------------------------------------------------

/// Convert winit modifier state to crossterm KeyModifiers.
pub fn translate_modifiers(mods: &winit::keyboard::ModifiersState) -> KeyModifiers {
    let mut result = KeyModifiers::NONE;
    if mods.shift_key() {
        result |= KeyModifiers::SHIFT;
    }
    if mods.control_key() {
        result |= KeyModifiers::CONTROL;
    }
    if mods.alt_key() {
        result |= KeyModifiers::ALT;
    }
    if mods.super_key() {
        result |= KeyModifiers::SUPER;
    }
    result
}

/// Translate a winit key event to a crossterm KeyEvent.
///
/// `alt_location` tracks which Alt/Option key is held, used on macOS to
/// distinguish Left Alt (international character composition) from Right Alt
/// (keyboard shortcut modifier).
pub fn translate_key_event(
    event: &winit::event::KeyEvent,
    modifiers: KeyModifiers,
    alt_location: Option<KeyLocation>,
) -> Option<CtKeyEvent> {
    let (effective_modifiers, alt_override_char) =
        if cfg!(target_os = "macos") && modifiers.contains(KeyModifiers::ALT) {
            match alt_location {
                Some(KeyLocation::Left) => {
                    // Left Alt: strip ALT so the composed character is treated
                    // as plain text input rather than a shortcut.
                    (modifiers & !KeyModifiers::ALT, None)
                }
                Some(KeyLocation::Right) => {
                    // Right Alt: keep ALT for shortcuts, but undo the macOS
                    // Option-composition by deriving the base character from
                    // the physical key.
                    let base = physical_key_to_base_char(
                        &event.physical_key,
                        modifiers.contains(KeyModifiers::SHIFT),
                    );
                    (modifiers, base)
                }
                _ => (modifiers, None),
            }
        } else {
            (modifiers, None)
        };

    let code = match &event.logical_key {
        Key::Named(named) => translate_named_key(named, &event.location, effective_modifiers)?,
        Key::Character(ch) => {
            let c = alt_override_char.unwrap_or_else(|| ch.chars().next().unwrap_or('\0'));
            if c == '\0' {
                return None;
            }
            if c == '\t' && effective_modifiers.contains(KeyModifiers::SHIFT) {
                KeyCode::BackTab
            } else {
                KeyCode::Char(c)
            }
        }
        Key::Dead(_) | Key::Unidentified(_) => return None,
    };

    Some(CtKeyEvent {
        code,
        modifiers: effective_modifiers,
        kind: KeyEventKind::Press,
        state: KeyEventState::NONE,
    })
}

/// Map a winit physical key to its base US-layout character.
///
/// Used on macOS to undo the Option-key composition for Right Alt shortcuts.
fn physical_key_to_base_char(key: &winit::keyboard::PhysicalKey, shift: bool) -> Option<char> {
    use winit::keyboard::KeyCode as WK;
    use winit::keyboard::PhysicalKey;

    let PhysicalKey::Code(code) = key else {
        return None;
    };

    let base = match code {
        WK::KeyA => 'a',
        WK::KeyB => 'b',
        WK::KeyC => 'c',
        WK::KeyD => 'd',
        WK::KeyE => 'e',
        WK::KeyF => 'f',
        WK::KeyG => 'g',
        WK::KeyH => 'h',
        WK::KeyI => 'i',
        WK::KeyJ => 'j',
        WK::KeyK => 'k',
        WK::KeyL => 'l',
        WK::KeyM => 'm',
        WK::KeyN => 'n',
        WK::KeyO => 'o',
        WK::KeyP => 'p',
        WK::KeyQ => 'q',
        WK::KeyR => 'r',
        WK::KeyS => 's',
        WK::KeyT => 't',
        WK::KeyU => 'u',
        WK::KeyV => 'v',
        WK::KeyW => 'w',
        WK::KeyX => 'x',
        WK::KeyY => 'y',
        WK::KeyZ => 'z',
        WK::Digit0 => '0',
        WK::Digit1 => '1',
        WK::Digit2 => '2',
        WK::Digit3 => '3',
        WK::Digit4 => '4',
        WK::Digit5 => '5',
        WK::Digit6 => '6',
        WK::Digit7 => '7',
        WK::Digit8 => '8',
        WK::Digit9 => '9',
        WK::Minus => '-',
        WK::Equal => '=',
        WK::BracketLeft => '[',
        WK::BracketRight => ']',
        WK::Backslash => '\\',
        WK::Semicolon => ';',
        WK::Quote => '\'',
        WK::Comma => ',',
        WK::Period => '.',
        WK::Slash => '/',
        WK::Backquote => '`',
        _ => return None,
    };

    if shift && base.is_ascii_alphabetic() {
        Some(base.to_ascii_uppercase())
    } else {
        Some(base)
    }
}

/// Translate a winit NamedKey to a crossterm KeyCode.
pub fn translate_named_key(
    key: &NamedKey,
    location: &winit::keyboard::KeyLocation,
    modifiers: KeyModifiers,
) -> Option<KeyCode> {
    use winit::keyboard::KeyLocation;

    Some(match key {
        // Navigation
        NamedKey::ArrowUp => KeyCode::Up,
        NamedKey::ArrowDown => KeyCode::Down,
        NamedKey::ArrowLeft => KeyCode::Left,
        NamedKey::ArrowRight => KeyCode::Right,
        NamedKey::Home => KeyCode::Home,
        NamedKey::End => KeyCode::End,
        NamedKey::PageUp => KeyCode::PageUp,
        NamedKey::PageDown => KeyCode::PageDown,

        // Editing
        NamedKey::Backspace => KeyCode::Backspace,
        NamedKey::Delete => KeyCode::Delete,
        NamedKey::Insert => KeyCode::Insert,
        NamedKey::Enter => KeyCode::Enter,
        NamedKey::Tab => {
            if modifiers.contains(KeyModifiers::SHIFT) {
                KeyCode::BackTab
            } else {
                KeyCode::Tab
            }
        }
        NamedKey::Space => KeyCode::Char(' '),
        NamedKey::Escape => KeyCode::Esc,

        // Function keys
        NamedKey::F1 => KeyCode::F(1),
        NamedKey::F2 => KeyCode::F(2),
        NamedKey::F3 => KeyCode::F(3),
        NamedKey::F4 => KeyCode::F(4),
        NamedKey::F5 => KeyCode::F(5),
        NamedKey::F6 => KeyCode::F(6),
        NamedKey::F7 => KeyCode::F(7),
        NamedKey::F8 => KeyCode::F(8),
        NamedKey::F9 => KeyCode::F(9),
        NamedKey::F10 => KeyCode::F(10),
        NamedKey::F11 => KeyCode::F(11),
        NamedKey::F12 => KeyCode::F(12),
        NamedKey::F13 => KeyCode::F(13),
        NamedKey::F14 => KeyCode::F(14),
        NamedKey::F15 => KeyCode::F(15),
        NamedKey::F16 => KeyCode::F(16),
        NamedKey::F17 => KeyCode::F(17),
        NamedKey::F18 => KeyCode::F(18),
        NamedKey::F19 => KeyCode::F(19),
        NamedKey::F20 => KeyCode::F(20),
        NamedKey::F21 => KeyCode::F(21),
        NamedKey::F22 => KeyCode::F(22),
        NamedKey::F23 => KeyCode::F(23),
        NamedKey::F24 => KeyCode::F(24),
        NamedKey::F25 => KeyCode::F(25),
        NamedKey::F26 => KeyCode::F(26),
        NamedKey::F27 => KeyCode::F(27),
        NamedKey::F28 => KeyCode::F(28),
        NamedKey::F29 => KeyCode::F(29),
        NamedKey::F30 => KeyCode::F(30),
        NamedKey::F31 => KeyCode::F(31),
        NamedKey::F32 => KeyCode::F(32),
        NamedKey::F33 => KeyCode::F(33),
        NamedKey::F34 => KeyCode::F(34),
        NamedKey::F35 => KeyCode::F(35),

        // Lock keys
        NamedKey::CapsLock => KeyCode::CapsLock,
        NamedKey::NumLock => KeyCode::NumLock,
        NamedKey::ScrollLock => KeyCode::ScrollLock,

        // Misc
        NamedKey::PrintScreen => KeyCode::PrintScreen,
        NamedKey::Pause => KeyCode::Pause,
        NamedKey::ContextMenu => KeyCode::Menu,

        // Media keys
        NamedKey::MediaPlay => KeyCode::Media(MediaKeyCode::Play),
        NamedKey::MediaPause => KeyCode::Media(MediaKeyCode::Pause),
        NamedKey::MediaPlayPause => KeyCode::Media(MediaKeyCode::PlayPause),
        NamedKey::MediaStop => KeyCode::Media(MediaKeyCode::Stop),
        NamedKey::MediaTrackNext => KeyCode::Media(MediaKeyCode::TrackNext),
        NamedKey::MediaTrackPrevious => KeyCode::Media(MediaKeyCode::TrackPrevious),
        NamedKey::MediaFastForward => KeyCode::Media(MediaKeyCode::FastForward),
        NamedKey::MediaRewind => KeyCode::Media(MediaKeyCode::Rewind),
        NamedKey::MediaRecord => KeyCode::Media(MediaKeyCode::Record),
        NamedKey::AudioVolumeDown => KeyCode::Media(MediaKeyCode::LowerVolume),
        NamedKey::AudioVolumeUp => KeyCode::Media(MediaKeyCode::RaiseVolume),
        NamedKey::AudioVolumeMute => KeyCode::Media(MediaKeyCode::MuteVolume),

        // Modifier keys emitted as KeyCode::Modifier with left/right
        NamedKey::Shift => {
            let side = match location {
                KeyLocation::Right => ModifierKeyCode::RightShift,
                _ => ModifierKeyCode::LeftShift,
            };
            KeyCode::Modifier(side)
        }
        NamedKey::Control => {
            let side = match location {
                KeyLocation::Right => ModifierKeyCode::RightControl,
                _ => ModifierKeyCode::LeftControl,
            };
            KeyCode::Modifier(side)
        }
        NamedKey::Alt => {
            let side = match location {
                KeyLocation::Right => ModifierKeyCode::RightAlt,
                _ => ModifierKeyCode::LeftAlt,
            };
            KeyCode::Modifier(side)
        }
        NamedKey::Super => {
            let side = match location {
                KeyLocation::Right => ModifierKeyCode::RightSuper,
                _ => ModifierKeyCode::LeftSuper,
            };
            KeyCode::Modifier(side)
        }
        NamedKey::Hyper => {
            let side = match location {
                KeyLocation::Right => ModifierKeyCode::RightHyper,
                _ => ModifierKeyCode::LeftHyper,
            };
            KeyCode::Modifier(side)
        }
        NamedKey::Meta => {
            let side = match location {
                KeyLocation::Right => ModifierKeyCode::RightMeta,
                _ => ModifierKeyCode::LeftMeta,
            };
            KeyCode::Modifier(side)
        }

        // All other named keys — skip
        _ => return None,
    })
}

/// Translate a winit mouse button to a crossterm mouse button.
pub fn translate_mouse_button(button: MouseButton) -> Option<CtMouseButton> {
    match button {
        MouseButton::Left => Some(CtMouseButton::Left),
        MouseButton::Right => Some(CtMouseButton::Right),
        MouseButton::Middle => Some(CtMouseButton::Middle),
        _ => None,
    }
}

/// Convert pixel coordinates to terminal cell coordinates.
pub fn pixel_to_cell(pixel: (f64, f64), cell_size: (f64, f64)) -> (u16, u16) {
    let col = (pixel.0 / cell_size.0.max(1.0)) as u16;
    let row = (pixel.1 / cell_size.1.max(1.0)) as u16;
    (col, row)
}

/// Convert window pixel dimensions to terminal grid dimensions (cols, rows).
pub fn cell_dimensions_to_grid(width: f64, height: f64, cell_size: (f64, f64)) -> (u16, u16) {
    let cols = (width / cell_size.0.max(1.0)) as u16;
    let rows = (height / cell_size.1.max(1.0)) as u16;
    (cols.max(1), rows.max(1))
}

/// Decode the embedded 256×256 PNG icon into a winit `Icon`.
fn load_window_icon() -> Option<winit::window::Icon> {
    decode_png_rgba(ICON_PNG)
        .and_then(|(rgba, w, h)| winit::window::Icon::from_rgba(rgba, w, h).ok())
}

/// Decode a PNG image to RGBA bytes.
/// Returns (rgba_bytes, width, height) or None on failure.
fn decode_png_rgba(data: &[u8]) -> Option<(Vec<u8>, u32, u32)> {
    let decoder = png::Decoder::new(std::io::Cursor::new(data));
    let mut reader = decoder.read_info().ok()?;
    let mut buf = vec![0u8; reader.output_buffer_size()?];
    let info = reader.next_frame(&mut buf).ok()?;
    buf.truncate(info.buffer_size());

    // Convert to RGBA if needed.
    let rgba = match info.color_type {
        png::ColorType::Rgba => buf,
        png::ColorType::Rgb => {
            let mut rgba = Vec::with_capacity(buf.len() / 3 * 4);
            for chunk in buf.chunks_exact(3) {
                rgba.extend_from_slice(chunk);
                rgba.push(255);
            }
            rgba
        }
        _ => return None,
    };

    Some((rgba, info.width, info.height))
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    #[test]
    fn test_named_key_navigation() {
        let loc = KeyLocation::Standard;
        let mods = KeyModifiers::NONE;
        assert_eq!(
            translate_named_key(&NamedKey::ArrowUp, &loc, mods),
            Some(KeyCode::Up)
        );
        assert_eq!(
            translate_named_key(&NamedKey::ArrowDown, &loc, mods),
            Some(KeyCode::Down)
        );
        assert_eq!(
            translate_named_key(&NamedKey::ArrowLeft, &loc, mods),
            Some(KeyCode::Left)
        );
        assert_eq!(
            translate_named_key(&NamedKey::ArrowRight, &loc, mods),
            Some(KeyCode::Right)
        );
        assert_eq!(
            translate_named_key(&NamedKey::Home, &loc, mods),
            Some(KeyCode::Home)
        );
        assert_eq!(
            translate_named_key(&NamedKey::End, &loc, mods),
            Some(KeyCode::End)
        );
        assert_eq!(
            translate_named_key(&NamedKey::PageUp, &loc, mods),
            Some(KeyCode::PageUp)
        );
        assert_eq!(
            translate_named_key(&NamedKey::PageDown, &loc, mods),
            Some(KeyCode::PageDown)
        );
    }

    #[test]
    fn test_named_key_editing() {
        let loc = KeyLocation::Standard;
        let mods = KeyModifiers::NONE;
        assert_eq!(
            translate_named_key(&NamedKey::Backspace, &loc, mods),
            Some(KeyCode::Backspace)
        );
        assert_eq!(
            translate_named_key(&NamedKey::Delete, &loc, mods),
            Some(KeyCode::Delete)
        );
        assert_eq!(
            translate_named_key(&NamedKey::Insert, &loc, mods),
            Some(KeyCode::Insert)
        );
        assert_eq!(
            translate_named_key(&NamedKey::Enter, &loc, mods),
            Some(KeyCode::Enter)
        );
        assert_eq!(
            translate_named_key(&NamedKey::Escape, &loc, mods),
            Some(KeyCode::Esc)
        );
        assert_eq!(
            translate_named_key(&NamedKey::Space, &loc, mods),
            Some(KeyCode::Char(' '))
        );
    }

    #[test]
    fn test_tab_and_backtab() {
        let loc = KeyLocation::Standard;
        assert_eq!(
            translate_named_key(&NamedKey::Tab, &loc, KeyModifiers::NONE),
            Some(KeyCode::Tab)
        );
        assert_eq!(
            translate_named_key(&NamedKey::Tab, &loc, KeyModifiers::SHIFT),
            Some(KeyCode::BackTab)
        );
    }

    #[test]
    fn test_function_keys() {
        let loc = KeyLocation::Standard;
        let mods = KeyModifiers::NONE;
        assert_eq!(
            translate_named_key(&NamedKey::F1, &loc, mods),
            Some(KeyCode::F(1))
        );
        assert_eq!(
            translate_named_key(&NamedKey::F5, &loc, mods),
            Some(KeyCode::F(5))
        );
        assert_eq!(
            translate_named_key(&NamedKey::F12, &loc, mods),
            Some(KeyCode::F(12))
        );
        assert_eq!(
            translate_named_key(&NamedKey::F24, &loc, mods),
            Some(KeyCode::F(24))
        );
        assert_eq!(
            translate_named_key(&NamedKey::F35, &loc, mods),
            Some(KeyCode::F(35))
        );
    }

    #[test]
    fn test_lock_and_misc_keys() {
        let loc = KeyLocation::Standard;
        let mods = KeyModifiers::NONE;
        assert_eq!(
            translate_named_key(&NamedKey::CapsLock, &loc, mods),
            Some(KeyCode::CapsLock)
        );
        assert_eq!(
            translate_named_key(&NamedKey::NumLock, &loc, mods),
            Some(KeyCode::NumLock)
        );
        assert_eq!(
            translate_named_key(&NamedKey::ScrollLock, &loc, mods),
            Some(KeyCode::ScrollLock)
        );
        assert_eq!(
            translate_named_key(&NamedKey::PrintScreen, &loc, mods),
            Some(KeyCode::PrintScreen)
        );
        assert_eq!(
            translate_named_key(&NamedKey::Pause, &loc, mods),
            Some(KeyCode::Pause)
        );
        assert_eq!(
            translate_named_key(&NamedKey::ContextMenu, &loc, mods),
            Some(KeyCode::Menu)
        );
    }

    #[test]
    fn test_modifier_keys_left_right() {
        let mods = KeyModifiers::NONE;

        assert_eq!(
            translate_named_key(&NamedKey::Shift, &KeyLocation::Left, mods),
            Some(KeyCode::Modifier(ModifierKeyCode::LeftShift))
        );
        assert_eq!(
            translate_named_key(&NamedKey::Control, &KeyLocation::Left, mods),
            Some(KeyCode::Modifier(ModifierKeyCode::LeftControl))
        );
        assert_eq!(
            translate_named_key(&NamedKey::Alt, &KeyLocation::Left, mods),
            Some(KeyCode::Modifier(ModifierKeyCode::LeftAlt))
        );
        assert_eq!(
            translate_named_key(&NamedKey::Super, &KeyLocation::Left, mods),
            Some(KeyCode::Modifier(ModifierKeyCode::LeftSuper))
        );
        assert_eq!(
            translate_named_key(&NamedKey::Hyper, &KeyLocation::Left, mods),
            Some(KeyCode::Modifier(ModifierKeyCode::LeftHyper))
        );
        assert_eq!(
            translate_named_key(&NamedKey::Meta, &KeyLocation::Left, mods),
            Some(KeyCode::Modifier(ModifierKeyCode::LeftMeta))
        );

        assert_eq!(
            translate_named_key(&NamedKey::Shift, &KeyLocation::Right, mods),
            Some(KeyCode::Modifier(ModifierKeyCode::RightShift))
        );
        assert_eq!(
            translate_named_key(&NamedKey::Control, &KeyLocation::Right, mods),
            Some(KeyCode::Modifier(ModifierKeyCode::RightControl))
        );
        assert_eq!(
            translate_named_key(&NamedKey::Super, &KeyLocation::Right, mods),
            Some(KeyCode::Modifier(ModifierKeyCode::RightSuper))
        );
    }

    #[test]
    fn test_media_keys() {
        let loc = KeyLocation::Standard;
        let mods = KeyModifiers::NONE;
        assert_eq!(
            translate_named_key(&NamedKey::MediaPlay, &loc, mods),
            Some(KeyCode::Media(MediaKeyCode::Play))
        );
        assert_eq!(
            translate_named_key(&NamedKey::MediaPause, &loc, mods),
            Some(KeyCode::Media(MediaKeyCode::Pause))
        );
        assert_eq!(
            translate_named_key(&NamedKey::MediaPlayPause, &loc, mods),
            Some(KeyCode::Media(MediaKeyCode::PlayPause))
        );
        assert_eq!(
            translate_named_key(&NamedKey::MediaStop, &loc, mods),
            Some(KeyCode::Media(MediaKeyCode::Stop))
        );
        assert_eq!(
            translate_named_key(&NamedKey::AudioVolumeUp, &loc, mods),
            Some(KeyCode::Media(MediaKeyCode::RaiseVolume))
        );
        assert_eq!(
            translate_named_key(&NamedKey::AudioVolumeDown, &loc, mods),
            Some(KeyCode::Media(MediaKeyCode::LowerVolume))
        );
        assert_eq!(
            translate_named_key(&NamedKey::AudioVolumeMute, &loc, mods),
            Some(KeyCode::Media(MediaKeyCode::MuteVolume))
        );
    }

    #[test]
    fn test_unknown_named_key_returns_none() {
        let loc = KeyLocation::Standard;
        let mods = KeyModifiers::NONE;
        assert_eq!(
            translate_named_key(&NamedKey::BrowserBack, &loc, mods),
            None
        );
        assert_eq!(translate_named_key(&NamedKey::LaunchMail, &loc, mods), None);
    }

    #[test]
    fn test_translate_modifiers_none() {
        let mods = winit::keyboard::ModifiersState::empty();
        assert_eq!(translate_modifiers(&mods), KeyModifiers::NONE);
    }

    #[test]
    fn test_translate_modifiers_all() {
        let mods = winit::keyboard::ModifiersState::SHIFT
            | winit::keyboard::ModifiersState::CONTROL
            | winit::keyboard::ModifiersState::ALT
            | winit::keyboard::ModifiersState::SUPER;
        let result = translate_modifiers(&mods);
        assert!(result.contains(KeyModifiers::SHIFT));
        assert!(result.contains(KeyModifiers::CONTROL));
        assert!(result.contains(KeyModifiers::ALT));
        assert!(result.contains(KeyModifiers::SUPER));
    }

    #[test]
    fn test_translate_modifiers_single() {
        assert_eq!(
            translate_modifiers(&winit::keyboard::ModifiersState::CONTROL),
            KeyModifiers::CONTROL
        );
        assert_eq!(
            translate_modifiers(&winit::keyboard::ModifiersState::ALT),
            KeyModifiers::ALT
        );
        assert_eq!(
            translate_modifiers(&winit::keyboard::ModifiersState::SUPER),
            KeyModifiers::SUPER
        );
    }

    #[test]
    fn test_translate_mouse_buttons() {
        assert_eq!(
            translate_mouse_button(MouseButton::Left),
            Some(CtMouseButton::Left)
        );
        assert_eq!(
            translate_mouse_button(MouseButton::Right),
            Some(CtMouseButton::Right)
        );
        assert_eq!(
            translate_mouse_button(MouseButton::Middle),
            Some(CtMouseButton::Middle)
        );
        assert_eq!(translate_mouse_button(MouseButton::Back), None);
        assert_eq!(translate_mouse_button(MouseButton::Forward), None);
        assert_eq!(translate_mouse_button(MouseButton::Other(42)), None);
    }

    #[test]
    fn test_pixel_to_cell_basic() {
        let cell_size = (10.0, 20.0);
        assert_eq!(pixel_to_cell((0.0, 0.0), cell_size), (0, 0));
        assert_eq!(pixel_to_cell((10.0, 20.0), cell_size), (1, 1));
        assert_eq!(pixel_to_cell((25.0, 45.0), cell_size), (2, 2));
        assert_eq!(pixel_to_cell((99.0, 199.0), cell_size), (9, 9));
    }

    #[test]
    fn test_pixel_to_cell_zero_cell_size() {
        let result = pixel_to_cell((100.0, 100.0), (0.0, 0.0));
        assert_eq!(result, (100, 100));
    }

    #[test]
    fn test_cell_dimensions_to_grid() {
        let cell_size = (14.4, 28.8);
        let (cols, rows) = cell_dimensions_to_grid(1280.0, 800.0, cell_size);
        assert_eq!(cols, 88);
        assert_eq!(rows, 27);
    }

    #[test]
    fn test_cell_dimensions_to_grid_minimum() {
        let (cols, rows) = cell_dimensions_to_grid(1.0, 1.0, (14.4, 28.8));
        assert_eq!(cols, 1);
        assert_eq!(rows, 1);
    }

    #[test]
    fn test_cell_dimensions_to_grid_zero_size() {
        let (cols, rows) = cell_dimensions_to_grid(0.0, 0.0, (14.4, 28.8));
        assert_eq!(cols, 1);
        assert_eq!(rows, 1);
    }

    #[test]
    fn test_physical_key_to_base_char_letters() {
        use winit::keyboard::{KeyCode as WK, PhysicalKey};
        assert_eq!(
            physical_key_to_base_char(&PhysicalKey::Code(WK::KeyA), false),
            Some('a')
        );
        assert_eq!(
            physical_key_to_base_char(&PhysicalKey::Code(WK::KeyZ), false),
            Some('z')
        );
        assert_eq!(
            physical_key_to_base_char(&PhysicalKey::Code(WK::KeyF), true),
            Some('F')
        );
    }

    #[test]
    fn test_physical_key_to_base_char_digits() {
        use winit::keyboard::{KeyCode as WK, PhysicalKey};
        assert_eq!(
            physical_key_to_base_char(&PhysicalKey::Code(WK::Digit0), false),
            Some('0')
        );
        assert_eq!(
            physical_key_to_base_char(&PhysicalKey::Code(WK::Digit9), false),
            Some('9')
        );
        assert_eq!(
            physical_key_to_base_char(&PhysicalKey::Code(WK::Digit5), true),
            Some('5')
        );
    }

    #[test]
    fn test_physical_key_to_base_char_punctuation() {
        use winit::keyboard::{KeyCode as WK, PhysicalKey};
        assert_eq!(
            physical_key_to_base_char(&PhysicalKey::Code(WK::Comma), false),
            Some(',')
        );
        assert_eq!(
            physical_key_to_base_char(&PhysicalKey::Code(WK::Slash), false),
            Some('/')
        );
        assert_eq!(
            physical_key_to_base_char(&PhysicalKey::Code(WK::Backquote), false),
            Some('`')
        );
    }

    #[test]
    fn test_physical_key_to_base_char_unknown_returns_none() {
        use winit::keyboard::{KeyCode as WK, PhysicalKey};
        assert_eq!(
            physical_key_to_base_char(&PhysicalKey::Code(WK::Enter), false),
            None
        );
        assert_eq!(
            physical_key_to_base_char(&PhysicalKey::Code(WK::Space), false),
            None
        );
    }
}
