#import "MASShortcut.h"

NSString *const MASShortcutKeyCode = @"KeyCode";
NSString *const MASShortcutModifierFlags = @"ModifierFlags";

@implementation MASShortcut {
    NSUInteger _keyCode; // NSNotFound if empty
    NSUInteger _modifierFlags; // 0 if empty
}

@synthesize modifierFlags = _modifierFlags;
@synthesize keyCode = _keyCode;

#pragma mark -

+ (BOOL)supportsSecureCoding
{
    return YES;
}

- (void)encodeWithCoder:(NSCoder *)coder
{
    [coder encodeInteger:(self.keyCode != NSNotFound ? (NSInteger)self.keyCode : - 1) forKey:MASShortcutKeyCode];
    [coder encodeInteger:(NSInteger)self.modifierFlags forKey:MASShortcutModifierFlags];
}

- (id)initWithCoder:(NSCoder *)decoder
{
    self = [super init];
    if (self) {
        NSInteger code = [decoder decodeIntegerForKey:MASShortcutKeyCode];
        self.keyCode = (code < 0 ? NSNotFound : (NSUInteger)code);
        self.modifierFlags = [decoder decodeIntegerForKey:MASShortcutModifierFlags];
    }
    return self;
}

- (id)initWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags
{
    self = [super init];
    if (self) {
        _keyCode = code;
        _modifierFlags = MASShortcutClear(flags);
    }
    return self;
}

+ (MASShortcut *)shortcutWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags
{
    return [[self alloc] initWithKeyCode:code modifierFlags:flags];
}

+ (MASShortcut *)shortcutWithEvent:(NSEvent *)event
{
    return [[self alloc] initWithKeyCode:event.keyCode modifierFlags:event.modifierFlags];
}

+ (MASShortcut *)shortcutWithData:(NSData *)data
{
    id shortcut = (data ? [NSKeyedUnarchiver unarchiveObjectWithData:data] : nil);
    return shortcut;
}

#pragma mark - Shortcut accessors

- (NSData *)data
{
    return [NSKeyedArchiver archivedDataWithRootObject:self];
}

- (void)setModifierFlags:(NSUInteger)value
{
    _modifierFlags = MASShortcutClear(value);
}

- (UInt32)carbonKeyCode
{
    return (self.keyCode == NSNotFound ? 0 : (UInt32)self.keyCode);
}

- (UInt32)carbonFlags
{
    return MASShortcutCarbonFlags(self.modifierFlags);
}

- (NSString *)description
{
    return [NSString stringWithFormat:@"%@%@", self.modifierFlagsString, self.keyCodeString];
}

- (NSString *)keyCodeStringForKeyEquivalent
{
    NSString *keyCodeString = self.keyCodeString;
    if (keyCodeString.length > 1) {
        switch (self.keyCode) {
            case kVK_F1: return MASShortcutChar(0xF704);
            case kVK_F2: return MASShortcutChar(0xF705);
            case kVK_F3: return MASShortcutChar(0xF706);
            case kVK_F4: return MASShortcutChar(0xF707);
            case kVK_F5: return MASShortcutChar(0xF708);
            case kVK_F6: return MASShortcutChar(0xF709);
            case kVK_F7: return MASShortcutChar(0xF70a);
            case kVK_F8: return MASShortcutChar(0xF70b);
            case kVK_F9: return MASShortcutChar(0xF70c);
            case kVK_F10: return MASShortcutChar(0xF70d);
            case kVK_F11: return MASShortcutChar(0xF70e);
            case kVK_F12: return MASShortcutChar(0xF70f);
            // From this point down I am guessing F13 etc come sequentially, I don't have a keyboard to test.
            case kVK_F13: return MASShortcutChar(0xF710);
            case kVK_F14: return MASShortcutChar(0xF711);
            case kVK_F15: return MASShortcutChar(0xF712);
            case kVK_F16: return MASShortcutChar(0xF713);
            case kVK_F17: return MASShortcutChar(0xF714);
            case kVK_F18: return MASShortcutChar(0xF715);
            case kVK_F19: return MASShortcutChar(0xF716);
            case kVK_Space: return MASShortcutChar(0x20);
            default: return @"";
        }
    }
    return keyCodeString.lowercaseString;
}

- (NSString *)keyCodeString
{
    // Some key codes don't have an equivalent
    switch (self.keyCode) {
        case NSNotFound: return @"";
        case kVK_F1: return @"F1";
        case kVK_F2: return @"F2";
        case kVK_F3: return @"F3";
        case kVK_F4: return @"F4";
        case kVK_F5: return @"F5";
        case kVK_F6: return @"F6";
        case kVK_F7: return @"F7";
        case kVK_F8: return @"F8";
        case kVK_F9: return @"F9";
        case kVK_F10: return @"F10";
        case kVK_F11: return @"F11";
        case kVK_F12: return @"F12";
        case kVK_F13: return @"F13";
        case kVK_F14: return @"F14";
        case kVK_F15: return @"F15";
        case kVK_F16: return @"F16";
        case kVK_F17: return @"F17";
        case kVK_F18: return @"F18";
        case kVK_F19: return @"F19";
        case kVK_Space: return NSLocalizedString(@"Space", @"Shortcut glyph name for SPACE key");
        case kVK_Escape: return MASShortcutChar(kMASShortcutGlyphEscape);
        case kVK_Delete: return MASShortcutChar(kMASShortcutGlyphDeleteLeft);
        case kVK_ForwardDelete: return MASShortcutChar(kMASShortcutGlyphDeleteRight);
        case kVK_LeftArrow: return MASShortcutChar(kMASShortcutGlyphLeftArrow);
        case kVK_RightArrow: return MASShortcutChar(kMASShortcutGlyphRightArrow);
        case kVK_UpArrow: return MASShortcutChar(kMASShortcutGlyphUpArrow);
        case kVK_DownArrow: return MASShortcutChar(kMASShortcutGlyphDownArrow);
        case kVK_Help: return MASShortcutChar(kMASShortcutGlyphHelp);
        case kVK_PageUp: return MASShortcutChar(kMASShortcutGlyphPageUp);
        case kVK_PageDown: return MASShortcutChar(kMASShortcutGlyphPageDown);
        case kVK_Tab: return MASShortcutChar(kMASShortcutGlyphTabRight);
        case kVK_Return: return MASShortcutChar(kMASShortcutGlyphReturnR2L);
            
        // Keypad
        case kVK_ANSI_Keypad0: return @"0";
        case kVK_ANSI_Keypad1: return @"1";
        case kVK_ANSI_Keypad2: return @"2";
        case kVK_ANSI_Keypad3: return @"3";
        case kVK_ANSI_Keypad4: return @"4";
        case kVK_ANSI_Keypad5: return @"5";
        case kVK_ANSI_Keypad6: return @"6";
        case kVK_ANSI_Keypad7: return @"7";
        case kVK_ANSI_Keypad8: return @"8";
        case kVK_ANSI_Keypad9: return @"9";
        case kVK_ANSI_KeypadDecimal: return @".";
        case kVK_ANSI_KeypadMultiply: return @"*";
        case kVK_ANSI_KeypadPlus: return @"+";
        case kVK_ANSI_KeypadClear: return MASShortcutChar(kMASShortcutGlyphPadClear);
        case kVK_ANSI_KeypadDivide: return @"/";
        case kVK_ANSI_KeypadEnter: return MASShortcutChar(kMASShortcutGlyphReturn);
        case kVK_ANSI_KeypadMinus: return @"–";
        case kVK_ANSI_KeypadEquals: return @"=";
            
        // Hardcode
        case 119: return MASShortcutChar(kMASShortcutGlyphSoutheastArrow);
        case 115: return MASShortcutChar(kMASShortcutGlyphNorthwestArrow);
    }
    
    // Everything else should be printable so look it up in the current keyboard
    OSStatus error = noErr;
    NSString *keystroke = nil;
    TISInputSourceRef inputSource = TISCopyCurrentKeyboardLayoutInputSource();
    if (inputSource) {
        CFDataRef layoutDataRef = TISGetInputSourceProperty(inputSource, kTISPropertyUnicodeKeyLayoutData);
        if (layoutDataRef) {
            UCKeyboardLayout *layoutData = (UCKeyboardLayout *)CFDataGetBytePtr(layoutDataRef);
            UniCharCount length = 0;
            UniChar  chars[256] = { 0 };
            UInt32 deadKeyState = 0;
            error = UCKeyTranslate(layoutData, (UInt16)self.keyCode, kUCKeyActionDisplay, 0, // No modifiers
                                   LMGetKbdType(), kUCKeyTranslateNoDeadKeysMask, &deadKeyState,
                                   sizeof(chars) / sizeof(UniChar), &length, chars);
            keystroke = ((error == noErr) && length ? [NSString stringWithCharacters:chars length:length] : @"");
        }
        CFRelease(inputSource);
    }
    
    // Validate keystroke
    if (keystroke.length) {
        static NSMutableCharacterSet *validChars = nil;
        if (validChars == nil) {
            validChars = [[NSMutableCharacterSet alloc] init];
            [validChars formUnionWithCharacterSet:[NSCharacterSet alphanumericCharacterSet]];
            [validChars formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]];
            [validChars formUnionWithCharacterSet:[NSCharacterSet symbolCharacterSet]];
        }
        for (NSUInteger i = 0, length = keystroke.length; i < length; i++) {
            if (![validChars characterIsMember:[keystroke characterAtIndex:i]]) {
                keystroke = @"";
                break;
            }
        }
    }
    
    // Finally, we've got a shortcut!
    return keystroke.uppercaseString;
}

- (NSString *)modifierFlagsString
{
    unichar chars[4];
    NSUInteger count = 0;
    // These are in the same order as the menu manager shows them
    if (self.modifierFlags & NSEventModifierFlagControl) chars[count++] = kControlUnicode;
    if (self.modifierFlags & NSEventModifierFlagOption) chars[count++] = kOptionUnicode;
    if (self.modifierFlags & NSEventModifierFlagShift) chars[count++] = kShiftUnicode;
    if (self.modifierFlags & NSEventModifierFlagCommand) chars[count++] = kCommandUnicode;
    return (count ? [NSString stringWithCharacters:chars length:count] : @"");
}

#pragma mark - Validation logic

- (BOOL)shouldBypass
{
    NSString *codeString = self.keyCodeString;
    return (self.modifierFlags == NSEventModifierFlagCommand) && ([codeString isEqualToString:@"W"] || [codeString isEqualToString:@"Q"]);
}

BOOL MASShortcutAllowsAnyHotkeyWithOptionModifier = NO;

+ (void)setAllowsAnyHotkeyWithOptionModifier:(BOOL)allow
{
    MASShortcutAllowsAnyHotkeyWithOptionModifier = allow;
}

+ (BOOL)allowsAnyHotkeyWithOptionModifier
{
    return MASShortcutAllowsAnyHotkeyWithOptionModifier;
}

- (BOOL)isValid
{
    // Allow any function key with any combination of modifiers
    BOOL includesFunctionKey = ((_keyCode == kVK_F1) || (_keyCode == kVK_F2) || (_keyCode == kVK_F3) || (_keyCode == kVK_F4) ||
                                (_keyCode == kVK_F5) || (_keyCode == kVK_F6) || (_keyCode == kVK_F7) || (_keyCode == kVK_F8) ||
                                (_keyCode == kVK_F9) || (_keyCode == kVK_F10) || (_keyCode == kVK_F11) || (_keyCode == kVK_F12) ||
                                (_keyCode == kVK_F13) || (_keyCode == kVK_F14) || (_keyCode == kVK_F15) || (_keyCode == kVK_F16) ||
                                (_keyCode == kVK_F17) || (_keyCode == kVK_F18) || (_keyCode == kVK_F19) || (_keyCode == kVK_F20));
    if (includesFunctionKey) return YES;

    // Do not allow any other key without modifiers
    BOOL hasModifierFlags = (_modifierFlags > 0);
    if (!hasModifierFlags) return NO;

    // Allow any hotkey containing Control or Command modifier
    BOOL includesCommand = ((_modifierFlags & NSEventModifierFlagCommand) > 0);
    BOOL includesControl = ((_modifierFlags & NSEventModifierFlagControl) > 0);
    if (includesCommand || includesControl) return YES;

    // Allow Option key only in selected cases
    BOOL includesOption = ((_modifierFlags & NSEventModifierFlagOption) > 0);
    if (includesOption) {

        // Always allow Option-Space and Option-Escape because they do not have any bind system commands
        if ((_keyCode == kVK_Space) || (_keyCode == kVK_Escape)) return YES;

        // Allow Option modifier with any key even if it will break the system binding
        if ([[self class] allowsAnyHotkeyWithOptionModifier]) return YES;
    }

    // The hotkey does not have any modifiers or violates system bindings
    return NO;
}

- (BOOL)isKeyEquivalent:(NSString *)keyEquivalent flags:(NSUInteger)flags takenInMenu:(NSMenu *)menu error:(NSError **)outError
{
    for (NSMenuItem *menuItem in menu.itemArray) {
        if (menuItem.hasSubmenu && [self isKeyEquivalent:keyEquivalent flags:flags takenInMenu:menuItem.submenu error:outError]) return YES;
        
        BOOL equalFlags = (MASShortcutClear(menuItem.keyEquivalentModifierMask) == flags);
        BOOL equalHotkeyLowercase = [menuItem.keyEquivalent.lowercaseString isEqualToString:keyEquivalent];
        
        // Check if the cases are different, we know ours is lower and that shift is included in our modifiers
        // If theirs is capitol, we need to add shift to their modifiers
        if (equalHotkeyLowercase && ![menuItem.keyEquivalent isEqualToString:keyEquivalent]) {
            equalFlags = (MASShortcutClear(menuItem.keyEquivalentModifierMask | NSShiftKeyMask) == flags);
        }
        
        if (equalFlags && equalHotkeyLowercase) {
            if (outError) {
                NSString *format = NSLocalizedString(@"This shortcut cannot be used because it is already used by the menu item ‘%@’.",
                                                     @"Message for alert when shortcut is already used");
                NSDictionary *info = [NSDictionary dictionaryWithObject:[NSString stringWithFormat:format, menuItem.title]
                                                                 forKey:NSLocalizedDescriptionKey];
                *outError = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:info];
            }
            return YES;
        }
    }
    return NO;
}

- (BOOL)isTakenError:(NSError **)outError
{
	CFArrayRef globalHotKeys;
	BOOL isTaken = NO;
	if (CopySymbolicHotKeys(&globalHotKeys) == noErr) {

        // Enumerate all global hotkeys and check if any of them matches current shortcut
        for (CFIndex i = 0, count = CFArrayGetCount(globalHotKeys); i < count; i++) {
            CFDictionaryRef hotKeyInfo = CFArrayGetValueAtIndex(globalHotKeys, i);
            CFNumberRef code = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyCode);
            CFNumberRef flags = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyModifiers);
            CFNumberRef enabled = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyEnabled);

            if (([(__bridge NSNumber *)code unsignedIntegerValue] == self.keyCode) &&
                ([(__bridge NSNumber *)flags unsignedIntegerValue] == self.carbonFlags) &&
                ([(__bridge NSNumber *)enabled boolValue])) {

                if (outError) {
                    NSString *description = NSLocalizedString(@"This combination cannot be used because it is already used by a system-wide "
                                                              @"keyboard shortcut.\nIf you really want to use this key combination, most shortcuts "
                                                              @"can be changed in the Keyboard & Mouse panel in System Preferences.",
                                                              @"Message for alert when shortcut is already used by the system");
                    NSDictionary *info = [NSDictionary dictionaryWithObject:description forKey:NSLocalizedDescriptionKey];
                    *outError = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:info];
                }
                isTaken = YES;
                break;
            }
        }
        CFRelease(globalHotKeys);
    }
    return (isTaken || [self isKeyEquivalent:self.keyCodeStringForKeyEquivalent flags:self.modifierFlags takenInMenu:[NSApp mainMenu] error:outError]);
}

@end
