import AppKit
import Carbon.HIToolbox

/// Serializable representation of a keyboard shortcut (modifier flags + key code).
/// Stored in UserDefaults as a JSON string.
struct HotkeyBinding: Codable, Equatable, Hashable {
    var modifiers: UInt   // NSEvent.ModifierFlags.rawValue, masked to cmd/shift/ctrl/opt
    var keyCode: UInt16

    /// Checks whether the given NSEvent matches this binding.
    func matches(_ event: NSEvent) -> Bool {
        let mask = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
        let relevantFlags: NSEvent.ModifierFlags = [.command, .shift, .control, .option]
        return mask.intersection(relevantFlags).rawValue == modifiers
            && event.keyCode == keyCode
    }

    /// Human-readable label like "⌘⏎" or "⌘⇧T".
    var displayString: String {
        var parts: [String] = []
        let flags = NSEvent.ModifierFlags(rawValue: modifiers)
        if flags.contains(.control) { parts.append("⌃") }
        if flags.contains(.option)  { parts.append("⌥") }
        if flags.contains(.shift)   { parts.append("⇧") }
        if flags.contains(.command) { parts.append("⌘") }
        parts.append(keyName)
        return parts.joined()
    }

    private var keyName: String {
        switch keyCode {
        case 36:  return "⏎"
        case 48:  return "⇥"
        case 49:  return "Space"
        case 51:  return "⌫"
        case 53:  return "⎋"
        case 123: return "←"
        case 124: return "→"
        case 125: return "↓"
        case 126: return "↑"
        default:
            // Try to get the character from the key code
            let src = TISCopyCurrentKeyboardInputSource().takeRetainedValue()
            let layoutDataRef = TISGetInputSourceProperty(src, kTISPropertyUnicodeKeyLayoutData)
            if let layoutDataRef = layoutDataRef {
                let layoutData = unsafeBitCast(layoutDataRef, to: CFData.self) as Data
                var deadKeyState: UInt32 = 0
                var length = 0
                var chars = [UniChar](repeating: 0, count: 4)
                layoutData.withUnsafeBytes { ptr in
                    let layoutPtr = ptr.baseAddress!.assumingMemoryBound(to: UCKeyboardLayout.self)
                    UCKeyTranslate(
                        layoutPtr,
                        keyCode,
                        UInt16(kUCKeyActionDisplay),
                        0, // no modifiers for the base character
                        UInt32(LMGetKbdType()),
                        UInt32(kUCKeyTranslateNoDeadKeysBit),
                        &deadKeyState,
                        4,
                        &length,
                        &chars
                    )
                }
                if length > 0 {
                    return String(utf16CodeUnits: chars, count: length).uppercased()
                }
            }
            return "Key\(keyCode)"
        }
    }

    // MARK: - Serialization

    func toJSON() -> String {
        (try? String(data: JSONEncoder().encode(self), encoding: .utf8)) ?? "{}"
    }

    static func fromJSON(_ string: String) -> HotkeyBinding? {
        guard let data = string.data(using: .utf8) else { return nil }
        return try? JSONDecoder().decode(HotkeyBinding.self, from: data)
    }

    // MARK: - Presets

    static let cmdReturn    = HotkeyBinding(modifiers: NSEvent.ModifierFlags.command.rawValue, keyCode: 36)
    static let cmdT         = HotkeyBinding(modifiers: NSEvent.ModifierFlags.command.rawValue, keyCode: 17)
    static let cmdW         = HotkeyBinding(modifiers: NSEvent.ModifierFlags.command.rawValue, keyCode: 13)
    static let cmdShiftRB   = HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: 30)  // ]
    static let cmdShiftLB   = HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: 33)  // [
    static let cmdShiftDown = HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: 125)
    static let cmdShiftUp   = HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: 126)
    static let cmdD         = HotkeyBinding(modifiers: NSEvent.ModifierFlags.command.rawValue, keyCode: 2)

    static func cmdShiftDigit(_ digit: Int) -> HotkeyBinding? {
        guard let keyCode = keyCode(forDigit: digit) else { return nil }
        return HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: keyCode)
    }

    static func keyCode(forDigit digit: Int) -> UInt16? {
        switch digit {
        case 0: return 29
        case 1: return 18
        case 2: return 19
        case 3: return 20
        case 4: return 21
        case 5: return 23
        case 6: return 22
        case 7: return 26
        case 8: return 28
        case 9: return 25
        default: return nil
        }
    }
}
