#import "MASShortcutView.h"
#import "MASShortcut.h"

#define HINT_BUTTON_WIDTH 23.0
#define BUTTON_FONT_SIZE 11.0
#define SEGMENT_CHROME_WIDTH 6.0

#pragma mark -

@interface MASShortcutView () // Private accessors

@property (nonatomic, getter = isHinting) BOOL hinting;
@property (nonatomic, copy) NSString *shortcutPlaceholder;

@end

#pragma mark -

@implementation MASShortcutView {
    NSButtonCell *_shortcutCell;
    NSInteger _shortcutToolTipTag;
    NSInteger _hintToolTipTag;
    NSTrackingArea *_hintArea;
}

@synthesize enabled = _enabled;
@synthesize hinting = _hinting;
@synthesize shortcutValue = _shortcutValue;
@synthesize shortcutPlaceholder = _shortcutPlaceholder;
@synthesize shortcutValueChange = _shortcutValueChange;
@synthesize recording = _recording;
@synthesize appearance = _appearance;

#pragma mark -

+ (Class)shortcutCellClass
{
    return [NSButtonCell class];
}

- (id)initWithFrame:(CGRect)frameRect
{
    self = [super initWithFrame:frameRect];
    if (self) {
        [self commonInit];
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)coder
{
    self = [super initWithCoder:coder];
    if (self) {
        [self commonInit];
    }
    return self;
}

- (void)commonInit
{
    _shortcutCell = [[[self.class shortcutCellClass] alloc] init];
    _shortcutCell.buttonType = NSButtonTypePushOnPushOff;
    _shortcutCell.font = [[NSFontManager sharedFontManager] convertFont:_shortcutCell.font toSize:BUTTON_FONT_SIZE];
    _enabled = YES;
    [self resetShortcutCellStyle];
}

- (void)dealloc
{
    [self activateEventMonitoring:NO];
    [self activateResignObserver:NO];
}

#pragma mark - Public accessors

- (void)setEnabled:(BOOL)flag
{
    if (_enabled != flag) {
        _enabled = flag;
        [self updateTrackingAreas];
        self.recording = NO;
        [self setNeedsDisplay:YES];
    }
}

- (void)setAppearance:(MASShortcutViewAppearance)appearance
{
    if (_appearance != appearance) {
        _appearance = appearance;
        [self resetShortcutCellStyle];
        [self setNeedsDisplay:YES];
    }
}

- (void)resetShortcutCellStyle
{
    switch (_appearance) {
        case MASShortcutViewAppearanceDefault: {
            _shortcutCell.bezelStyle = NSBezelStyleRoundRect;
            break;
        }
        case MASShortcutViewAppearanceTexturedRect: {
            _shortcutCell.bezelStyle = NSBezelStyleTexturedRounded;
            break;
        }
        case MASShortcutViewAppearanceRounded: {
            _shortcutCell.bezelStyle = NSBezelStyleRounded;
            break;
        }
    }
}

- (void)setRecording:(BOOL)flag
{
    // Only one recorder can be active at the moment
    static MASShortcutView *currentRecorder = nil;
    if (flag && (currentRecorder != self)) {
        currentRecorder.recording = NO;
        currentRecorder = flag ? self : nil;
    }
    
    // Only enabled view supports recording
    if (flag && !self.enabled) return;
    
    if (_recording != flag) {
        _recording = flag;
        self.shortcutPlaceholder = nil;
        [self resetToolTips];
        [self activateEventMonitoring:_recording];
        [self activateResignObserver:_recording];
        [self setNeedsDisplay:YES];
    }
}

- (void)setShortcutValue:(MASShortcut *)shortcutValue
{
    _shortcutValue = shortcutValue;
    [self resetToolTips];
    [self setNeedsDisplay:YES];

    if (self.shortcutValueChange) {
        self.shortcutValueChange(self);
    }
}

- (void)setShortcutPlaceholder:(NSString *)shortcutPlaceholder
{
    _shortcutPlaceholder = shortcutPlaceholder.copy;
    [self setNeedsDisplay:YES];
}

#pragma mark - Drawing

- (BOOL)isFlipped
{
    return YES;
}

- (void)drawInRect:(CGRect)frame withTitle:(NSString *)title alignment:(NSTextAlignment)alignment state:(NSInteger)state
{
    _shortcutCell.title = title;
    _shortcutCell.alignment = alignment;
    _shortcutCell.state = state;
    _shortcutCell.enabled = self.enabled;

    switch (_appearance) {
        case MASShortcutViewAppearanceDefault: {
            [_shortcutCell drawWithFrame:frame inView:self];
            break;
        }
        case MASShortcutViewAppearanceTexturedRect: {
            [_shortcutCell drawWithFrame:CGRectOffset(frame, 0.0, 1.0) inView:self];
            break;
        }
        case MASShortcutViewAppearanceRounded: {
            [_shortcutCell drawWithFrame:CGRectOffset(frame, 0.0, 1.0) inView:self];
            break;
        }
    }
}

- (void)drawRect:(CGRect)dirtyRect
{
    if (self.shortcutValue) {
        [self drawInRect:self.bounds withTitle:MASShortcutChar(self.recording ? kMASShortcutGlyphEscape : kMASShortcutGlyphDeleteLeft)
               alignment:NSTextAlignmentRight state:NSControlStateValueOff];
        
        CGRect shortcutRect;
        [self getShortcutRect:&shortcutRect hintRect:NULL];
        NSString *title = (self.recording
                           ? (_hinting
                              ? NSLocalizedString(@"Use Old Shortcut", @"Cancel action button for non-empty shortcut in recording state")
                              : (self.shortcutPlaceholder.length > 0
                                 ? self.shortcutPlaceholder
                                 : NSLocalizedString(@"Type New Shortcut", @"Non-empty shortcut button in recording state")))
                           : _shortcutValue ? _shortcutValue.description : @"");
        [self drawInRect:shortcutRect withTitle:title alignment:NSTextAlignmentCenter state:self.isRecording ? NSControlStateValueOn : NSControlStateValueOff];
    }
    else {
        if (self.recording)
        {
            [self drawInRect:self.bounds withTitle:MASShortcutChar(kMASShortcutGlyphEscape) alignment:NSTextAlignmentRight state:NSControlStateValueOff];
            
            CGRect shortcutRect;
            [self getShortcutRect:&shortcutRect hintRect:NULL];
            NSString *title = (_hinting
                               ? NSLocalizedString(@"Cancel", @"Cancel action button in recording state")
                               : (self.shortcutPlaceholder.length > 0
                                  ? self.shortcutPlaceholder
                                  : NSLocalizedString(@"Type Shortcut", @"Empty shortcut button in recording state")));
            [self drawInRect:shortcutRect withTitle:title alignment:NSTextAlignmentCenter state:NSControlStateValueOn];
        }
        else
        {
            [self drawInRect:self.bounds withTitle:NSLocalizedString(@"Record Shortcut", @"Empty shortcut button in normal state")
                   alignment:NSTextAlignmentCenter state:NSControlStateValueOff];
        }
    }
}

#pragma mark - Mouse handling

- (void)getShortcutRect:(CGRect *)shortcutRectRef hintRect:(CGRect *)hintRectRef
{
    CGRect shortcutRect, hintRect;
    CGFloat hintButtonWidth = HINT_BUTTON_WIDTH;
    switch (self.appearance) {
        case MASShortcutViewAppearanceTexturedRect: hintButtonWidth += 2.0; break;
        case MASShortcutViewAppearanceRounded: hintButtonWidth += 3.0; break;
        default: break;
    }
    CGRectDivide(self.bounds, &hintRect, &shortcutRect, hintButtonWidth, CGRectMaxXEdge);
    if (shortcutRectRef)  *shortcutRectRef = shortcutRect;
    if (hintRectRef) *hintRectRef = hintRect;
}

- (BOOL)locationInShortcutRect:(CGPoint)location
{
    CGRect shortcutRect;
    [self getShortcutRect:&shortcutRect hintRect:NULL];
    return CGRectContainsPoint(shortcutRect, [self convertPoint:location fromView:nil]);
}

- (BOOL)locationInHintRect:(CGPoint)location
{
    CGRect hintRect;
    [self getShortcutRect:NULL hintRect:&hintRect];
    return CGRectContainsPoint(hintRect, [self convertPoint:location fromView:nil]);
}

- (void)mouseDown:(NSEvent *)event
{
    if (self.enabled) {
        if (self.shortcutValue) {
            if (self.recording) {
                if ([self locationInHintRect:event.locationInWindow]) {
                    self.recording = NO;
                }
            }
            else {
                if ([self locationInShortcutRect:event.locationInWindow]) {
                    self.recording = YES;
                }
                else {
                    self.shortcutValue = nil;
                }
            }
        }
        else {
            if (self.recording) {
                if ([self locationInHintRect:event.locationInWindow]) {
                    self.recording = NO;
                }
            }
            else {
                self.recording = YES;
            }
        }
    }
    else {
        [super mouseDown:event];
    }
}

#pragma mark - Handling mouse over

- (void)updateTrackingAreas
{
    [super updateTrackingAreas];
    
    if (_hintArea) {
        [self removeTrackingArea:_hintArea];
        _hintArea = nil;
    }
    
    // Forbid hinting if view is disabled
    if (!self.enabled) return;
    
    CGRect hintRect;
    [self getShortcutRect:NULL hintRect:&hintRect];
    NSTrackingAreaOptions options = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingAssumeInside);
    _hintArea = [[NSTrackingArea alloc] initWithRect:hintRect options:options owner:self userInfo:nil];
    [self addTrackingArea:_hintArea];
}

- (void)setHinting:(BOOL)flag
{
    if (_hinting != flag) {
        _hinting = flag;
        [self setNeedsDisplay:YES];
    }
}

- (void)mouseEntered:(NSEvent *)event
{
    self.hinting = YES;
}

- (void)mouseExited:(NSEvent *)event
{
    self.hinting = NO;
}

void *kUserDataShortcut = &kUserDataShortcut;
void *kUserDataHint = &kUserDataHint;

- (void)resetToolTips
{
    if (_shortcutToolTipTag) {
        (void)([self removeToolTip:_shortcutToolTipTag]), _shortcutToolTipTag = 0;
    }
    if (_hintToolTipTag) {
        (void)([self removeToolTip:_hintToolTipTag]), _hintToolTipTag = 0;
    }
    
    if ((self.shortcutValue == nil) || self.recording || !self.enabled) return;

    CGRect shortcutRect, hintRect;
    [self getShortcutRect:&shortcutRect hintRect:&hintRect];
    _shortcutToolTipTag = [self addToolTipRect:shortcutRect owner:self userData:kUserDataShortcut];
    _hintToolTipTag = [self addToolTipRect:hintRect owner:self userData:kUserDataHint];
}

- (NSString *)view:(NSView *)view stringForToolTip:(NSToolTipTag)tag point:(CGPoint)point userData:(void *)data
{
    if (data == kUserDataShortcut) {
        return NSLocalizedString(@"Click to record new shortcut", @"Tooltip for non-empty shortcut button");
    }
    else if (data == kUserDataHint) {
        return NSLocalizedString(@"Delete shortcut", @"Tooltip for hint button near the non-empty shortcut");
    }
    return nil;
}

#pragma mark - Event monitoring

- (void)activateEventMonitoring:(BOOL)shouldActivate
{
    static BOOL isActive = NO;
    if (isActive == shouldActivate) return;
    isActive = shouldActivate;
    
    static id eventMonitor = nil;
    if (shouldActivate) {
        __weak MASShortcutView *weakSelf = self;
        NSEventMask eventMask = (NSEventMaskKeyDown | NSEventMaskFlagsChanged);
        eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:eventMask handler:^(NSEvent *event) {

            MASShortcut *shortcut = [MASShortcut shortcutWithEvent:event];
            if ((shortcut.keyCode == kVK_Delete) || (shortcut.keyCode == kVK_ForwardDelete)) {
                // Delete shortcut
                weakSelf.shortcutValue = nil;
                weakSelf.recording = NO;
                event = nil;
            }
            else if (shortcut.keyCode == kVK_Escape && !shortcut.modifierFlags) {
                // Cancel recording
                weakSelf.recording = NO;
                event = nil;
            }
            else if (shortcut.shouldBypass) {
                // Command + W, Command + Q, ESC should deactivate recorder
                weakSelf.recording = NO;
            }
            else {
                // Verify possible shortcut
                if (shortcut.keyCodeString.length > 0) {
                    if (shortcut.valid) {
                        // Verify that shortcut is not used
                        NSError *error = nil;
                        if ([shortcut isTakenError:&error]) {
                            // Prevent cancel of recording when Alert window is key
                            [weakSelf activateResignObserver:NO];
                            [weakSelf activateEventMonitoring:NO];
                            NSString *format = NSLocalizedString(@"The key combination %@ cannot be used",
                                                                 @"Title for alert when shortcut is already used");
                            NSAlert *alert = [[NSAlert alloc] init];
                            alert.alertStyle = NSAlertStyleCritical;
                            alert.informativeText = [NSString stringWithFormat:format, shortcut];
                            alert.messageText = error.localizedDescription;
                            [alert addButtonWithTitle:NSLocalizedString(@"OK", @"Alert button when shortcut is already used")];
                            
                            [alert runModal];
                            weakSelf.shortcutPlaceholder = nil;
                            [weakSelf activateResignObserver:YES];
                            [weakSelf activateEventMonitoring:YES];
                        }
                        else {
                            weakSelf.shortcutValue = shortcut;
                            weakSelf.recording = NO;
                        }
                    }
                    else {
                        // Key press with or without SHIFT is not valid input
                        NSBeep();
                    }
                }
                else {
                    // User is playing with modifier keys
                    weakSelf.shortcutPlaceholder = shortcut.modifierFlagsString;
                }
                event = nil;
            }
            return event;
        }];
    }
    else {
        [NSEvent removeMonitor:eventMonitor];
    }
}

- (void)activateResignObserver:(BOOL)shouldActivate
{
    static BOOL isActive = NO;
    if (isActive == shouldActivate) return;
    isActive = shouldActivate;
    
    static id observer = nil;
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    if (shouldActivate) {
        __weak MASShortcutView *weakSelf = self;
        observer = [notificationCenter addObserverForName:NSWindowDidResignKeyNotification object:self.window
                                                queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) {
                                                    weakSelf.recording = NO;
                                                }];
    }
    else {
        [notificationCenter removeObserver:observer];
    }
}

@end
