// ModifierBar.swift
// Horizontal accessory bar providing sticky Ctrl, common special keys,
// arrow keys with long-press repeat, and a clipboard paste action.
//
// The bar communicates exclusively through the `onSend` closure — it never
// imports or references `WebSocketClient` directly, keeping it fully
// testable in isolation.

import SwiftUI

// MARK: - Ctrl key name map

/// Named tmux/terminal keys for Ctrl+letter combos.
///
/// Keys present here are sent as `{ type:"key", name:"ctrl-x" }`.
/// Characters absent from this map fall back to the raw control-byte
/// sequence sent via `{ type:"keys", data:"\x01" }`.
private let ctrlKeyMap: [Character: String] = [
    "a": "ctrl-a", "b": "ctrl-b", "c": "ctrl-c", "d": "ctrl-d",
    "e": "ctrl-e", "f": "ctrl-f", "g": "ctrl-g", "h": "ctrl-h",
    "i": "ctrl-i", "j": "ctrl-j", "k": "ctrl-k", "l": "ctrl-l",
    "m": "ctrl-m", "n": "ctrl-n", "o": "ctrl-o", "p": "ctrl-p",
    "q": "ctrl-q", "r": "ctrl-r", "s": "ctrl-s", "t": "ctrl-t",
    "u": "ctrl-u", "v": "ctrl-v", "w": "ctrl-w", "x": "ctrl-x",
    "y": "ctrl-y", "z": "ctrl-z",
]

// MARK: - ModifierBar

/// Horizontal accessory bar: `[Ctrl][Esc][Tab][←][↑][↓][→][⇧↵][📋]`
///
/// - **Ctrl**: sticky modifier. When armed (highlighted), the next special key
///   is sent prefixed with `ctrl-` (e.g. `ctrl-escape`). After dispatch the
///   modifier disarms automatically.
/// - **Esc / Tab / ⇧↵**: send the corresponding named key via `onSend`.
/// - **← ↑ ↓ →**: single tap sends the key; long-press (≥ 400 ms) repeats
///   every 100 ms while the button is held.
/// - **📋**: presents `PasteSheet` for a confirm-before-paste flow.
@MainActor
struct ModifierBar: View {

    // MARK: State

    @StateObject private var modifierState = ModifierState()

    // MARK: Public interface

    /// Called when a frame should be sent to the server.
    let onSend: (ClientToServer) -> Void

    // MARK: Private state

    @State private var showPasteSheet = false

    // MARK: Body

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 6) {
                // ── Ctrl ─────────────────────────────────────────────
                BarButton(
                    title: "Ctrl",
                    isActive: modifierState.ctrlActive
                ) {
                    modifierState.toggleCtrl()
                }

                Divider()
                    .frame(height: 20)

                // ── Static special keys ───────────────────────────────
                BarButton(title: "Esc")  { sendKey("escape") }
                BarButton(title: "Tab")  { sendKey("tab") }

                Divider()
                    .frame(height: 20)

                // ── Arrow keys (long-press repeat) ────────────────────
                RepeatingBarButton(title: "←") { sendKey("left")  }
                    .onRepeatStateChanged { repeating in
                        modifierState.isRepeating = repeating
                    }

                RepeatingBarButton(title: "↑") { sendKey("up")    }
                    .onRepeatStateChanged { repeating in
                        modifierState.isRepeating = repeating
                    }

                RepeatingBarButton(title: "↓") { sendKey("down")  }
                    .onRepeatStateChanged { repeating in
                        modifierState.isRepeating = repeating
                    }

                RepeatingBarButton(title: "→") { sendKey("right") }
                    .onRepeatStateChanged { repeating in
                        modifierState.isRepeating = repeating
                    }

                Divider()
                    .frame(height: 20)

                // ── Shift+Enter ───────────────────────────────────────
                BarButton(title: "⇧↵") { sendKey("shift-enter") }

                // ── Paste ─────────────────────────────────────────────
                BarButton(title: "📋") {
                    showPasteSheet = true
                }
                .sheet(isPresented: $showPasteSheet) {
                    PasteSheet(isPresented: $showPasteSheet, onSend: onSend)
                }
            }
            .padding(.horizontal, 8)
            .padding(.vertical, 4)
        }
    }

    // MARK: Key dispatch

    /// Builds and dispatches a key frame, applying the sticky Ctrl modifier
    /// when armed, then resets all modifiers.
    ///
    /// For named special keys the modifier is prepended as a prefix
    /// (e.g. `ctrl-escape`, `ctrl-up`). For single-character inputs the
    /// `ctrlKeyMap` is consulted first; unknown characters fall back to the
    /// raw control-byte sequence via `.keys`.
    private func sendKey(_ name: String) {
        if modifierState.ctrlActive {
            // Special-key path: prefix with "ctrl-"
            let ctrlName = "ctrl-\(name)"
            onSend(.key(name: ctrlName))
        } else {
            onSend(.key(name: name))
        }
        modifierState.reset()
    }

    // MARK: - Public helper (for external keyboard integration)

    /// Resolves a raw character typed on a hardware/software keyboard into a
    /// `ClientToServer` frame, applying the Ctrl modifier when armed.
    ///
    /// Callers may use this when integrating `ModifierBar` with a `TextField`
    /// or `UITextView` delegate to honour the sticky Ctrl state.
    ///
    /// - Returns: The frame to send, or `nil` if `ctrlActive` is `false` and
    ///   the caller should handle the character normally.
    func frameForCharacter(_ char: Character) -> ClientToServer? {
        guard modifierState.ctrlActive else { return nil }
        defer { modifierState.reset() }
        let lower = Character(char.lowercased())
        if let keyName = ctrlKeyMap[lower] {
            return .key(name: keyName)
        }
        // Fall back: compute raw control-byte (e.g. Ctrl+A = 0x01)
        if let scalar = lower.asciiValue, scalar >= 0x61, scalar <= 0x7A {
            let controlByte = scalar - 0x60          // 'a'=0x61 → 0x01
            let raw = String(UnicodeScalar(controlByte))
            return .keys(data: raw)
        }
        return nil
    }
}

// MARK: - BarButton

/// A uniformly styled pill button used in `ModifierBar`.
@MainActor
private struct BarButton: View {

    let title: String
    var isActive: Bool = false
    let action: () -> Void

    var body: some View {
        Button(action: action) {
            Text(title)
                .font(.system(size: 14, weight: .medium, design: .monospaced))
                .foregroundStyle(isActive ? Color.white : Color.primary)
                .padding(.horizontal, 10)
                .padding(.vertical, 6)
                .background(
                    RoundedRectangle(cornerRadius: 6, style: .continuous)
                        .fill(isActive
                              ? Color.accentColor
                              : Color(uiColor: .systemGray5))
                )
        }
        .buttonStyle(.plain)   // prevent SwiftUI from wrapping in extra chrome
    }
}

// MARK: - RepeatingBarButton

/// A bar button that fires its `action` immediately on touch-down, then
/// repeats every 100 ms after an initial 400 ms delay while the finger
/// remains pressed.
@MainActor
struct RepeatingBarButton: View {

    let title: String
    let action: () -> Void

    /// Optional callback invoked when the repeat cycle starts (`true`) or
    /// stops (`false`). Use this to update `ModifierState.isRepeating`.
    private var repeatStateHandler: ((Bool) -> Void)?

    @State private var repeatTask: Task<Void, Never>?

    init(title: String, action: @escaping () -> Void) {
        self.title = title
        self.action = action
    }

    var body: some View {
        Text(title)
            .font(.system(size: 14, weight: .medium, design: .monospaced))
            .foregroundStyle(Color.primary)
            .padding(.horizontal, 10)
            .padding(.vertical, 6)
            .background(
                RoundedRectangle(cornerRadius: 6, style: .continuous)
                    .fill(Color(uiColor: .systemGray5))
            )
            // `onPressingChanged` fires `true` on touch-down and `false` on
            // touch-up. `minimumDuration: 10` makes the `perform` closure
            // unreachable in practice, so all logic lives in the callback.
            .onLongPressGesture(
                minimumDuration: 10,
                maximumDistance: 50,
                perform: { /* unreachable — intentional */ },
                onPressingChanged: { isPressing in
                    if isPressing {
                        action()  // immediate first fire on touch-down
                        startRepeating()
                    } else {
                        stopRepeating()
                    }
                }
            )
    }

    // MARK: Helpers

    private func startRepeating() {
        repeatTask?.cancel()
        repeatTask = Task { @MainActor in
            do {
                // Initial pause before the repeat cadence begins.
                try await Task.sleep(for: .milliseconds(400))
                repeatStateHandler?(true)
                while !Task.isCancelled {
                    action()
                    try await Task.sleep(for: .milliseconds(100))
                }
            } catch {
                // Task was cancelled — fall through to cleanup below.
            }
            repeatStateHandler?(false)
        }
    }

    private func stopRepeating() {
        repeatTask?.cancel()
        repeatTask = nil
        // The handler is called with `false` inside the task's catch/finally
        // path, but call it here too in case the task hadn't started yet.
        repeatStateHandler?(false)
    }

    // MARK: Modifier-style API

    /// Attaches a callback that is invoked when repeat mode starts/stops.
    func onRepeatStateChanged(_ handler: @escaping (Bool) -> Void) -> RepeatingBarButton {
        var copy = self
        copy.repeatStateHandler = handler
        return copy
    }
}
