// Hardware-keyboard navigation for the gamepad UI (iOS/iPadOS/macOS): arrows move, Return/Space
// activate, Esc backs out.
//
// Asked for by a field user on an iPad ("select games with keyboard arrows, enter to launch"). An
// iPad on a Magic Keyboard and a couch Mac are the same situation the console layout was built
// for — a screen driven from a distance with a fixed set of directional inputs — and the whole
// navigation model (a cursor, a confirm, a back) already exists here for the controller. A
// keyboard is just a third input onto it, alongside the pad poll and touch.
//
// tvOS is excluded: the focus engine already routes hardware-keyboard arrows into focus moves
// there, and these screens hand it navigation authority on purpose.
//
// The view must be FOCUSED to receive key presses, so this takes focus on appear. That is safe on
// exactly these screens because the gamepad UI has no system text fields to steal it from —
// GamepadKeyboard is a custom grid of keycaps, not a `TextField`.

import PunktfunkKit
import SwiftUI
#if os(iOS) || os(macOS)

extension View {
    /// Route arrows / Return / Esc into the same handlers the controller poll drives.
    ///
    /// `active` mirrors the caller's `isActive` controller gate: a screen that has handed the pad
    /// to something on top must not keep eating key presses either, or a covered launcher
    /// navigates behind the screen in front of it.
    func gamepadKeyNavigation(
        active: Bool = true,
        onMove: @escaping (GamepadMenuInput.Direction) -> Void,
        onConfirm: @escaping () -> Void,
        onBack: (() -> Void)? = nil
    ) -> some View {
        modifier(GamepadKeyNav(active: active, onMove: onMove, onConfirm: onConfirm, onBack: onBack))
    }
}

private struct GamepadKeyNav: ViewModifier {
    let active: Bool
    let onMove: (GamepadMenuInput.Direction) -> Void
    let onConfirm: () -> Void
    let onBack: (() -> Void)?

    @FocusState private var focused: Bool

    func body(content: Content) -> some View {
        content
            .focusable(active)
            // No focus ring: these screens draw their own cursor (the centred card, the focused
            // row), and a system ring around the whole scroll view on top of it reads as a bug.
            .focusEffectDisabled()
            .focused($focused)
            // Claim focus on appear, and re-claim it whenever this screen becomes the active one
            // again — a pushed screen popping off leaves the one underneath unfocused.
            .onAppear { focused = active }
            .onChange(of: active) { _, nowActive in
                if nowActive { focused = true }
            }
            .onKeyPress(.upArrow) { handle { onMove(.up) } }
            .onKeyPress(.downArrow) { handle { onMove(.down) } }
            .onKeyPress(.leftArrow) { handle { onMove(.left) } }
            .onKeyPress(.rightArrow) { handle { onMove(.right) } }
            .onKeyPress(.return) { handle(onConfirm) }
            .onKeyPress(.space) { handle(onConfirm) }
            .onKeyPress(.escape) {
                guard let onBack else { return .ignored }
                return handle(onBack)
            }
    }

    /// Run a handler only while this screen owns input, and report back whether the press was
    /// consumed. `.ignored` matters: an unhandled Esc still has to reach the `.cancelAction`
    /// shortcut that closes a macOS sheet (see GamepadAddHostView's hidden Cancel button).
    private func handle(_ action: () -> Void) -> KeyPress.Result {
        guard active else { return .ignored }
        action()
        return .handled
    }
}
#endif
