// input-injector.swift
// Persistent child process that reads commands from stdin and injects
// mouse / keyboard / scroll events via CoreGraphics CGEventPost.
//
// Protocol (one command per line on stdin):
//   m  x y          – move cursor
//   dd x y          – left button down (drag start)
//   du x y          – left button up   (drag end)
//   dm x y          – drag move (left held)
//   rc x y          – right click (down + up)
//   dc x y          – double click
//   scroll dy       – vertical scroll (positive = up)
//   scrollh dx      – horizontal scroll (positive = left)
//   key keysym 1|0  – key down (1) or up (0), keysym is X11 numeric
//
// Each command receives "ok\n" or "err <message>\n" on stdout.

import CoreGraphics
import Foundation

// ---------------------------------------------------------------------------
// X11 keysym -> macOS virtual keycode mapping
// ---------------------------------------------------------------------------

let keysymToKeycode: [UInt32: UInt16] = [
    // Letters a-z (XK_a = 0x0061 .. XK_z = 0x007a)
    0x0061: 0,   // a
    0x0062: 11,  // b
    0x0063: 8,   // c
    0x0064: 2,   // d
    0x0065: 14,  // e
    0x0066: 3,   // f
    0x0067: 5,   // g
    0x0068: 4,   // h
    0x0069: 34,  // i
    0x006a: 38,  // j
    0x006b: 40,  // k
    0x006c: 37,  // l
    0x006d: 46,  // m
    0x006e: 45,  // n
    0x006f: 31,  // o
    0x0070: 35,  // p
    0x0071: 12,  // q
    0x0072: 15,  // r
    0x0073: 1,   // s
    0x0074: 17,  // t
    0x0075: 32,  // u
    0x0076: 9,   // v
    0x0077: 13,  // w
    0x0078: 7,   // x
    0x0079: 16,  // y
    0x007a: 6,   // z

    // Upper-case letters (XK_A = 0x0041 .. XK_Z = 0x005a) — same keycodes
    0x0041: 0,   // A
    0x0042: 11,  // B
    0x0043: 8,   // C
    0x0044: 2,   // D
    0x0045: 14,  // E
    0x0046: 3,   // F
    0x0047: 5,   // G
    0x0048: 4,   // H
    0x0049: 34,  // I
    0x004a: 38,  // J
    0x004b: 40,  // K
    0x004c: 37,  // L
    0x004d: 46,  // M
    0x004e: 45,  // N
    0x004f: 31,  // O
    0x0050: 35,  // P
    0x0051: 12,  // Q
    0x0052: 15,  // R
    0x0053: 1,   // S
    0x0054: 17,  // T
    0x0055: 32,  // U
    0x0056: 9,   // V
    0x0057: 13,  // W
    0x0058: 7,   // X
    0x0059: 16,  // Y
    0x005a: 6,   // Z

    // Digits 0-9 (XK_0 = 0x0030 .. XK_9 = 0x0039)
    0x0030: 29,  // 0
    0x0031: 18,  // 1
    0x0032: 19,  // 2
    0x0033: 20,  // 3
    0x0034: 21,  // 4
    0x0035: 23,  // 5
    0x0036: 22,  // 6
    0x0037: 26,  // 7
    0x0038: 28,  // 8
    0x0039: 25,  // 9

    // Symbols
    0x0020: 49,  // space
    0x002d: 27,  // minus
    0x003d: 24,  // equal
    0x005b: 33,  // bracketleft
    0x005d: 30,  // bracketright
    0x005c: 42,  // backslash
    0x003b: 41,  // semicolon
    0x0027: 39,  // apostrophe
    0x0060: 50,  // grave
    0x002c: 43,  // comma
    0x002e: 47,  // period
    0x002f: 44,  // slash

    // Function keys (XK_F1 = 0xffbe .. XK_F12 = 0xffc9)
    0xffbe: 122, // F1
    0xffbf: 120, // F2
    0xffc0: 99,  // F3
    0xffc1: 118, // F4
    0xffc2: 96,  // F5
    0xffc3: 97,  // F6
    0xffc4: 98,  // F7
    0xffc5: 100, // F8
    0xffc6: 101, // F9
    0xffc7: 109, // F10
    0xffc8: 103, // F11
    0xffc9: 111, // F12

    // Control keys
    0xff0d: 36,  // Return
    0xff09: 48,  // Tab
    0xff1b: 53,  // Escape
    0xff08: 51,  // BackSpace
    0xffff: 117, // Delete (forward)

    // Navigation
    0xff50: 115, // Home
    0xff57: 119, // End
    0xff55: 116, // Page_Up
    0xff56: 121, // Page_Down
    0xff51: 123, // Left
    0xff52: 126, // Up
    0xff53: 124, // Right
    0xff54: 125, // Down

    // Modifiers (used for standalone key down/up events)
    0xffe1: 56,  // Shift_L
    0xffe2: 60,  // Shift_R
    0xffe3: 59,  // Control_L
    0xffe4: 62,  // Control_R
    0xffe9: 58,  // Alt_L  (Option)
    0xffea: 61,  // Alt_R  (Option)
    0xffe7: 55,  // Meta_L (Command)
    0xffe8: 54,  // Meta_R (Command)
    0xffe5: 57,  // Caps_Lock
]

// Modifier keysyms -> CGEventFlags
let keysymToFlag: [UInt32: CGEventFlags] = [
    0xffe1: .maskShift,
    0xffe2: .maskShift,
    0xffe3: .maskControl,
    0xffe4: .maskControl,
    0xffe9: .maskAlternate,
    0xffea: .maskAlternate,
    0xffe7: .maskCommand,
    0xffe8: .maskCommand,
]

// Track currently held modifier flags
var currentFlags: CGEventFlags = []

// Track left mouse button state for drag detection
var leftButtonDown = false

// ---------------------------------------------------------------------------
// Event injection helpers
// ---------------------------------------------------------------------------

func moveMouse(_ x: Double, _ y: Double) {
    let point = CGPoint(x: x, y: y)
    let eventType: CGEventType = leftButtonDown ? .leftMouseDragged : .mouseMoved
    guard let event = CGEvent(mouseEventSource: nil, mouseType: eventType,
                              mouseCursorPosition: point, mouseButton: .left) else { return }
    event.flags = currentFlags
    event.post(tap: .cghidEventTap)
}

func leftDown(_ x: Double, _ y: Double) {
    let point = CGPoint(x: x, y: y)
    guard let event = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown,
                              mouseCursorPosition: point, mouseButton: .left) else { return }
    event.flags = currentFlags
    event.post(tap: .cghidEventTap)
    leftButtonDown = true
}

func leftUp(_ x: Double, _ y: Double) {
    let point = CGPoint(x: x, y: y)
    guard let event = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp,
                              mouseCursorPosition: point, mouseButton: .left) else { return }
    event.flags = currentFlags
    event.post(tap: .cghidEventTap)
    leftButtonDown = false
}

func dragMove(_ x: Double, _ y: Double) {
    let point = CGPoint(x: x, y: y)
    guard let event = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDragged,
                              mouseCursorPosition: point, mouseButton: .left) else { return }
    event.flags = currentFlags
    event.post(tap: .cghidEventTap)
}

func rightClick(_ x: Double, _ y: Double) {
    let point = CGPoint(x: x, y: y)
    guard let down = CGEvent(mouseEventSource: nil, mouseType: .rightMouseDown,
                             mouseCursorPosition: point, mouseButton: .right) else { return }
    guard let up = CGEvent(mouseEventSource: nil, mouseType: .rightMouseUp,
                           mouseCursorPosition: point, mouseButton: .right) else { return }
    down.flags = currentFlags
    up.flags = currentFlags
    down.post(tap: .cghidEventTap)
    up.post(tap: .cghidEventTap)
}

func doubleClick(_ x: Double, _ y: Double) {
    let point = CGPoint(x: x, y: y)
    // First click
    guard let d1 = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown,
                           mouseCursorPosition: point, mouseButton: .left) else { return }
    guard let u1 = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp,
                           mouseCursorPosition: point, mouseButton: .left) else { return }
    d1.setIntegerValueField(.mouseEventClickState, value: 1)
    u1.setIntegerValueField(.mouseEventClickState, value: 1)
    d1.flags = currentFlags
    u1.flags = currentFlags
    d1.post(tap: .cghidEventTap)
    u1.post(tap: .cghidEventTap)
    // Second click
    guard let d2 = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown,
                           mouseCursorPosition: point, mouseButton: .left) else { return }
    guard let u2 = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp,
                           mouseCursorPosition: point, mouseButton: .left) else { return }
    d2.setIntegerValueField(.mouseEventClickState, value: 2)
    u2.setIntegerValueField(.mouseEventClickState, value: 2)
    d2.flags = currentFlags
    u2.flags = currentFlags
    d2.post(tap: .cghidEventTap)
    u2.post(tap: .cghidEventTap)
}

func scrollVertical(_ dy: Int32) {
    guard let event = CGEvent(scrollWheelEvent2Source: nil, units: .line,
                              wheelCount: 1, wheel1: dy, wheel2: 0, wheel3: 0) else { return }
    event.post(tap: .cghidEventTap)
}

func scrollHorizontal(_ dx: Int32) {
    guard let event = CGEvent(scrollWheelEvent2Source: nil, units: .line,
                              wheelCount: 2, wheel1: 0, wheel2: dx, wheel3: 0) else { return }
    event.post(tap: .cghidEventTap)
}

// ---------------------------------------------------------------------------
// macOS system actions via CGEvent (supports fn modifier key)
// ---------------------------------------------------------------------------

func sendFnKey(_ keycode: UInt16) {
    let src = CGEventSource(stateID: .combinedSessionState)
    guard let down = CGEvent(keyboardEventSource: src, virtualKey: CGKeyCode(keycode), keyDown: true),
          let up   = CGEvent(keyboardEventSource: src, virtualKey: CGKeyCode(keycode), keyDown: false)
    else { return }
    down.flags = .maskSecondaryFn
    up.flags   = .maskSecondaryFn
    down.post(tap: .cghidEventTap)
    usleep(50_000)
    up.post(tap: .cghidEventTap)
}

func runAppleScript(_ script: String) {
    let proc = Process()
    proc.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
    proc.arguments = ["-e", script]
    proc.standardOutput = FileHandle.nullDevice
    proc.standardError  = FileHandle.nullDevice
    try? proc.run()
    proc.waitUntilExit()
}

func performAction(_ name: String) {
    switch name {
    case "mission-control":
        // AppleScript via System Events — most reliable for Exposé
        runAppleScript("tell application \"System Events\" to key code 126 using control down")
    case "app-windows":
        runAppleScript("tell application \"System Events\" to key code 125 using control down")
    case "show-desktop":
        sendFnKey(103)          // Fn + F11 (key code 103)
    case "launchpad":
        // Use AppleScript — Fn+F4 via CGEvent doesn't work when hotkey is disabled
        runAppleScript("tell application \"System Events\" to key code 131")
    default:
        fputs("err unknown action: \(name)\n", stdout)
        fflush(stdout)
        return
    }
}

func keyEvent(_ keysym: UInt32, _ isDown: Bool) {
    // Handle modifier flag tracking
    if let flag = keysymToFlag[keysym] {
        if isDown {
            currentFlags.insert(flag)
        } else {
            currentFlags.remove(flag)
        }
    }

    guard let keycode = keysymToKeycode[keysym] else {
        // Try to handle printable ASCII characters by their character code
        if keysym >= 0x0020 && keysym <= 0x007e {
            // For printable ASCII, use the character directly
            // This is a fallback — the mapping table should cover most cases
            fputs("err unknown keysym 0x\(String(keysym, radix: 16))\n", stdout)
            fflush(stdout)
            return
        }
        fputs("err unknown keysym 0x\(String(keysym, radix: 16))\n", stdout)
        fflush(stdout)
        return
    }

    guard let event = CGEvent(keyboardEventSource: nil, virtualKey: CGKeyCode(keycode),
                              keyDown: isDown) else {
        fputs("err CGEvent creation failed\n", stdout)
        fflush(stdout)
        return
    }
    event.flags = currentFlags
    event.post(tap: .cghidEventTap)
}

// ---------------------------------------------------------------------------
// Main loop: read stdin line by line
// ---------------------------------------------------------------------------

// Disable stdout buffering for immediate responses
setbuf(stdout, nil)

fputs("ready\n", stdout)
fflush(stdout)

while let line = readLine() {
    let parts = line.trimmingCharacters(in: .whitespaces).split(separator: " ", maxSplits: 3)
    guard !parts.isEmpty else {
        fputs("err empty command\n", stdout)
        fflush(stdout)
        continue
    }

    let cmd = String(parts[0])

    switch cmd {
    case "m":
        guard parts.count >= 3,
              let x = Double(parts[1]),
              let y = Double(parts[2]) else {
            fputs("err usage: m x y\n", stdout)
            fflush(stdout)
            continue
        }
        moveMouse(x, y)

    case "dd":
        guard parts.count >= 3,
              let x = Double(parts[1]),
              let y = Double(parts[2]) else {
            fputs("err usage: dd x y\n", stdout)
            fflush(stdout)
            continue
        }
        leftDown(x, y)

    case "du":
        guard parts.count >= 3,
              let x = Double(parts[1]),
              let y = Double(parts[2]) else {
            fputs("err usage: du x y\n", stdout)
            fflush(stdout)
            continue
        }
        leftUp(x, y)

    case "dm":
        guard parts.count >= 3,
              let x = Double(parts[1]),
              let y = Double(parts[2]) else {
            fputs("err usage: dm x y\n", stdout)
            fflush(stdout)
            continue
        }
        dragMove(x, y)

    case "rc":
        guard parts.count >= 3,
              let x = Double(parts[1]),
              let y = Double(parts[2]) else {
            fputs("err usage: rc x y\n", stdout)
            fflush(stdout)
            continue
        }
        rightClick(x, y)

    case "dc":
        guard parts.count >= 3,
              let x = Double(parts[1]),
              let y = Double(parts[2]) else {
            fputs("err usage: dc x y\n", stdout)
            fflush(stdout)
            continue
        }
        doubleClick(x, y)

    case "scroll":
        guard parts.count >= 2,
              let dy = Int32(parts[1]) else {
            fputs("err usage: scroll dy\n", stdout)
            fflush(stdout)
            continue
        }
        scrollVertical(dy)

    case "scrollh":
        guard parts.count >= 2,
              let dx = Int32(parts[1]) else {
            fputs("err usage: scrollh dx\n", stdout)
            fflush(stdout)
            continue
        }
        scrollHorizontal(dx)

    case "key":
        guard parts.count >= 3,
              let keysym = UInt32(parts[1]),
              let downFlag = Int(parts[2]) else {
            fputs("err usage: key keysym 0|1\n", stdout)
            fflush(stdout)
            continue
        }
        keyEvent(keysym, downFlag != 0)

    case "action":
        guard parts.count >= 2 else {
            fputs("err usage: action <name>\n", stdout)
            fflush(stdout)
            continue
        }
        let name = String(parts[1])
        performAction(name)

    default:
        fputs("err unknown command: \(cmd)\n", stdout)
        fflush(stdout)
        continue
    }

    fputs("ok\n", stdout)
    fflush(stdout)
}
