// A controller-driven on-screen keyboard for the gamepad UI's text fields (iOS/iPadOS only) —
// iOS has no system keyboard a game controller can drive (the tvOS fullscreen entry doesn't
// exist here), so without this, adding a host from the couch would end with "now touch the
// screen". Dpad/stick moves a key cursor over a fixed grid, A types, X backspaces, B/Y confirms.
// Lowercase + digits + the hostname/address punctuation is deliberately the whole character set:
// these fields hold names, addresses and ports, not prose.
//
// Edits are applied to the binding live (the caller's field row shows every keystroke), so
// closing the keyboard is always "done" — there is no separate cancel/commit step to get wrong.
// Touch stays a fallback: every keycap is tappable. A HARDWARE keyboard (an iPad on a Magic
// Keyboard, a Mac) types straight into the field while the tray is up — characters insert,
// ⌫ deletes, arrows move the key cursor, Return and Esc are Done — with no system keyboard ever
// raised, because there is no text field to raise it.

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

struct GamepadKeyboard: View {
    @Environment(\.gamepadInk) private var ink
    @Binding var text: String
    /// Restricts typed characters (e.g. digits for a port field); backspace always works.
    var allowed: CharacterSet?
    /// B / Y / the Done key — the binding already holds the final text.
    let onDone: () -> Void

    @State private var input = GamepadMenuInput(manager: .shared)
    @State private var haptics = MenuHaptics(manager: .shared)
    @State private var cursor = GridPos(row: 1, col: 0) // opens on "q"
    @State private var pressTick = 0
    @State private var boundaryTick = 0
    /// Hardware-keyboard focus: the tray takes it on appear so key presses land here (the row
    /// list behind it hands its own key navigation off while editing).
    @FocusState private var hardwareFocus: Bool
    #if os(iOS)
    /// `.compact` (landscape phone): shorter keycaps so the tray leaves room for the field rows.
    @Environment(\.verticalSizeClass) private var vSizeClass

    private var compact: Bool { vSizeClass == .compact }
    #else
    private let compact = false // no size classes on macOS; the sheet is sized generously
    #endif

    private struct GridPos: Hashable {
        var row: Int
        var col: Int
    }

    private enum Key: Hashable {
        case char(Character)
        case space
        case backspace
        case done
    }

    /// Digits first (addresses/ports), then letters; the last char column carries the
    /// hostname/address punctuation.
    private static let rows: [[Key]] = [
        Array("1234567890").map(Key.char),
        Array("qwertyuiop").map(Key.char),
        Array("asdfghjkl-").map(Key.char),
        Array("zxcvbnm._:").map(Key.char),
        [.space, .backspace, .done],
    ]

    var body: some View {
        VStack(spacing: compact ? 5 : 7) {
            ForEach(Self.rows.indices, id: \.self) { r in
                HStack(spacing: compact ? 5 : 7) {
                    ForEach(Self.rows[r].indices, id: \.self) { c in
                        keycap(Self.rows[r][c], focused: cursor == GridPos(row: r, col: c))
                            .onTapGesture {
                                cursor = GridPos(row: r, col: c)
                                press(Self.rows[r][c])
                            }
                    }
                }
            }
        }
        .frame(maxWidth: 560)
        .padding(compact ? 10 : 14)
        .background {
            RoundedRectangle(cornerRadius: 22, style: .continuous)
                .fill(.ultraThinMaterial)
                .environment(\.colorScheme, .dark)
        }
        .overlay {
            RoundedRectangle(cornerRadius: 22, style: .continuous)
                .strokeBorder(ink.fg(0.12), lineWidth: 1)
        }
        .sensoryFeedback(.selection, trigger: cursor)
        .sensoryFeedback(.impact(weight: .light), trigger: pressTick)
        .sensoryFeedback(.impact(flexibility: .rigid, intensity: 0.7), trigger: boundaryTick)
        // Hardware keys. No focus ring — the tray draws its own cursor.
        .focusable()
        .focusEffectDisabled()
        .focused($hardwareFocus)
        .onKeyPress(phases: .down) { hardwareKey($0) }
        .onAppear {
            wire()
            input.start()
            hardwareFocus = true
        }
        .onDisappear {
            input.stop()
            haptics.stop()
        }
    }

    // MARK: - Keycaps

    @ViewBuilder private func keycap(_ key: Key, focused: Bool) -> some View {
        Group {
            switch key {
            case .char(let c):
                Text(String(c)).font(.geistFixed(18, .medium))
            case .space:
                Image(systemName: "space")
            case .backspace:
                Image(systemName: "delete.left")
            case .done:
                Label("Done", systemImage: "checkmark")
                    .font(.geist(15, .semibold, relativeTo: .callout))
            }
        }
        // The focused keycap sits on `ink.accent`, so `onAccent` is what reads on it — a dark
        // accent palette got black-on-dark with the old literal black.
        .foregroundStyle(focused ? ink.onAccent : ink.fg)
        .frame(maxWidth: .infinity, minHeight: compact ? 34 : 42)
        .background {
            RoundedRectangle(cornerRadius: 9, style: .continuous)
                .fill(focused ? AnyShapeStyle(ink.accent) : AnyShapeStyle(ink.fg(0.08)))
        }
        .animation(.smooth(duration: 0.12), value: focused)
        .contentShape(Rectangle())
    }

    // MARK: - Input

    private func wire() {
        input.onMove = { move($0) }
        input.onConfirm = { press(Self.rows[cursor.row][cursor.col]) }
        input.onTertiary = { press(.backspace) }
        input.onSecondary = onDone
        input.onBack = onDone
    }

    private func move(_ direction: GamepadMenuInput.Direction) {
        var next = cursor
        switch direction {
        case .left: next.col -= 1
        case .right: next.col += 1
        case .up, .down:
            let row = cursor.row + (direction == .down ? 1 : -1)
            guard row >= 0, row < Self.rows.count else { return refuse() }
            // Map the column proportionally between rows of different widths, so e.g. Done
            // (rightmost of 3) goes up to the rightmost letters, not to "e".
            let from = max(1, Self.rows[cursor.row].count - 1)
            let to = Self.rows[row].count - 1
            next = GridPos(
                row: row,
                col: Int((Double(cursor.col) * Double(to) / Double(from)).rounded()))
        }
        guard next.row >= 0, next.row < Self.rows.count,
              next.col >= 0, next.col < Self.rows[next.row].count
        else { return refuse() }
        cursor = next
        haptics.move()
    }

    private func press(_ key: Key) {
        switch key {
        case .char(let c):
            if let allowed, !c.unicodeScalars.allSatisfy(allowed.contains) { return refuse() }
            text.append(c)
        case .space:
            if let allowed, !allowed.contains(" ") { return refuse() }
            text.append(" ")
        case .backspace:
            guard !text.isEmpty else { return refuse() }
            text.removeLast()
        case .done:
            haptics.confirm()
            onDone()
            return
        }
        pressTick &+= 1
        haptics.move()
    }

    /// Refused input (edge of the grid, a disallowed character, deleting nothing).
    private func refuse() {
        boundaryTick &+= 1
        haptics.boundary()
    }

    /// A hardware key while the tray is up. Return and Esc are both Done — the binding already
    /// holds the text, so there is nothing to cancel; ⌫ deletes; arrows drive the key cursor
    /// (Return does NOT type the highlighted key — a keyboard user types letters, not caps);
    /// anything with ⌘ (⌘Q, ⌘W…) is left to the system.
    private func hardwareKey(_ key: KeyPress) -> KeyPress.Result {
        if key.modifiers.contains(.command) { return .ignored }
        switch key.key {
        case .escape, .return:
            haptics.confirm()
            onDone()
            return .handled
        case .delete, .deleteForward:
            DispatchQueue.main.async { press(.backspace) }
            return .handled
        case .leftArrow: move(.left); return .handled
        case .rightArrow: move(.right); return .handled
        case .upArrow: move(.up); return .handled
        case .downArrow: move(.down); return .handled
        case .space:
            DispatchQueue.main.async { press(.space) }
            return .handled
        default:
            break
        }
        // ⌫ can also arrive as a bare control character rather than a named key.
        if key.characters == "\u{7F}" || key.characters == "\u{08}" {
            DispatchQueue.main.async { press(.backspace) }
            return .handled
        }
        // Printable characters — including ones the on-screen grid doesn't offer (a capital,
        // an umlaut): the on-screen set is deliberately small, the field's own `allowed` set is
        // the real rule.
        let typed = key.characters
        guard !typed.isEmpty,
              typed.unicodeScalars.allSatisfy({ !CharacterSet.controlCharacters.contains($0) })
        else { return .ignored }
        if let allowed, !typed.unicodeScalars.allSatisfy(allowed.contains) {
            refuse()
            return .handled
        }
        // Off the key-event dispatch: writing the binding synchronously here re-rendered the
        // screen mid-delivery and a fast burst of keystrokes lost every other one.
        DispatchQueue.main.async {
            text.append(typed)
            pressTick &+= 1
            haptics.move()
        }
        return .handled
    }
}
#endif
