import AppKit
import Carbon.HIToolbox

/// A single global hotkey, registered through Carbon's RegisterEventHotKey.
///
/// Deliberately not NSEvent's global monitor: that needs Accessibility permission,
/// this doesn't. Used for the "test fire" key so scenes can be triggered on demand
/// while filming, without clicking the menu bar on camera.
final class HotKey {
    private var ref: EventHotKeyRef?
    private var handler: EventHandlerRef?
    private let action: () -> Void

    /// Shared box so the C callback can find the instance.
    private static var registry: [UInt32: HotKey] = [:]
    private static var nextId: UInt32 = 1

    private let id: UInt32

    /// - Parameters:
    ///   - keyCode: a `kVK_*` virtual key code.
    ///   - modifiers: Carbon modifier mask, e.g. `UInt32(cmdKey | optionKey | controlKey)`.
    init?(keyCode: Int, modifiers: UInt32, action: @escaping () -> Void) {
        self.action = action
        self.id = HotKey.nextId
        HotKey.nextId += 1

        var eventType = EventTypeSpec(eventClass: OSType(kEventClassKeyboard),
                                      eventKind: UInt32(kEventHotKeyPressed))

        let status = InstallEventHandler(
            GetApplicationEventTarget(),
            { _, event, _ -> OSStatus in
                var hotKeyID = EventHotKeyID()
                GetEventParameter(event, EventParamName(kEventParamDirectObject),
                                  EventParamType(typeEventHotKeyID), nil,
                                  MemoryLayout<EventHotKeyID>.size, nil, &hotKeyID)
                if let target = HotKey.registry[hotKeyID.id] {
                    DispatchQueue.main.async { target.action() }
                }
                return noErr
            },
            1, &eventType, nil, &handler)
        guard status == noErr else { return nil }

        let hotKeyID = EventHotKeyID(signature: OSType(0x414C_4E48), id: id) // 'ALNH'
        let registerStatus = RegisterEventHotKey(UInt32(keyCode), modifiers, hotKeyID,
                                                 GetApplicationEventTarget(), 0, &ref)
        guard registerStatus == noErr else {
            if let handler { RemoveEventHandler(handler) }
            return nil
        }
        HotKey.registry[id] = self
    }

    deinit {
        if let ref { UnregisterEventHotKey(ref) }
        if let handler { RemoveEventHandler(handler) }
        HotKey.registry[id] = nil
    }
}
