/*
 Copyright (c) 2011, Joachim Bengtsson
 All rights reserved.

 Redistribution and use in source and binary forms, with or without modification,
 are permitted provided that the following conditions are met:

 * Neither the name of the organization nor the names of its contributors may
   be used to endorse or promote products derived from this software without
   specific prior written permission.

 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

// Copyright (c) 2010 Spotify AB
#import "SPMediaKeyTap.h"

// Define to enable app list debug output
// #define DEBUG_SPMEDIAKEY_APPLIST 1

@interface SPMediaKeyTap () {
    CFMachPortRef _eventPort;
    CFRunLoopSourceRef _eventPortSource;
    __weak id<SPMediaKeyTapDelegate> _delegate;
    // The app that is frontmost in this list owns media keys
    NSMutableArray<NSRunningApplication *> *_mediaKeyAppList;
}

- (void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
- (void)startWatchingAppSwitching;
- (void)stopWatchingAppSwitching;
- (void)handleAndReleaseMediaKeyEvent:(NSEvent *)event;
@end

static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon);


// Inspired by http://gist.github.com/546311

@implementation SPMediaKeyTap

#pragma mark -
#pragma mark Setup and teardown

- (id)initWithDelegate:(id<SPMediaKeyTapDelegate>)delegate
{
    self = [super init];
    if (self) {
        NSAssert([delegate conformsToProtocol:@protocol(SPMediaKeyTapDelegate)],
            @"SPMediaKeyTap delegate must conform to the SPMediaKeyTapDelegate protocol!");
        _delegate = delegate;
        [self startWatchingAppSwitching];
        _mediaKeyAppList = [NSMutableArray new];
    }
    return self;
}

- (void)dealloc
{
    [self stopWatchingMediaKeys];
    [self stopWatchingAppSwitching];
}

- (void)startWatchingAppSwitching
{
    // Listen to "app switched" event, so that we don't intercept media keys if we
    // weren't the last "media key listening" app to be active

    [[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self
                                                           selector:@selector(frontmostAppChanged:)
                                                               name:NSWorkspaceDidActivateApplicationNotification
                                                             object:nil];


    [[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self
                                                           selector:@selector(appTerminated:)
                                                               name:NSWorkspaceDidTerminateApplicationNotification
                                                             object:nil];
}

- (void)stopWatchingAppSwitching
{
    [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
}

- (BOOL)startWatchingMediaKeys
{
    NSAssert([NSThread isMainThread],
        @"startWatchingMediaKeys must be called on the main thread!");

    // Prevent having multiple mediaKeys threads
    [self stopWatchingMediaKeys];

    [self setShouldInterceptMediaKeyEvents:YES];

    // Add an event tap to intercept the system defined media key events
    _eventPort = CGEventTapCreate(kCGSessionEventTap,
                                  kCGHeadInsertEventTap,
                                  kCGEventTapOptionDefault,
                                  CGEventMaskBit(NX_SYSDEFINED),
                                  tapEventCallback,
                                  (__bridge void * __nullable)(self));

    // Can be NULL if the app has no accessibility access permission
    if (_eventPort == NULL)
        return NO;

    _eventPortSource = CFMachPortCreateRunLoopSource(kCFAllocatorSystemDefault, _eventPort, 0);
    assert(_eventPortSource != NULL);

    if (_eventPortSource == NULL)
        return NO;

    CFRunLoopAddSource(CFRunLoopGetCurrent(), _eventPortSource, kCFRunLoopCommonModes);

    return YES;
}

- (void)stopWatchingMediaKeys
{
    NSAssert([NSThread isMainThread],
        @"stopWatchingMediaKeys must be called on the main thread!");

    // Remove runloop source
    if (_eventPortSource) {
        CFRunLoopRemoveSource(CFRunLoopGetCurrent(), _eventPortSource, kCFRunLoopCommonModes);
    }

    // Remove tap port
    if (_eventPort) {
        CFMachPortInvalidate(_eventPort);
        CFRelease(_eventPort);
        _eventPort = nil;
    }

    // Remove tap source
    if (_eventPortSource) {
        CFRelease(_eventPortSource);
        _eventPortSource = nil;
    }
}

#pragma mark -
#pragma mark Accessors

+ (NSArray*)mediaKeyUserBundleIdentifiers
{
    static NSArray *bundleIdentifiers;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSString *ourIdentifier = [[NSBundle mainBundle] bundleIdentifier];
        if (ourIdentifier == nil) {
            NSLog(@"SPMediaKeyTap: Bundle identifier unexpectedly nil, falling back to org.videolan.vlc");
            ourIdentifier = @"org.videolan.vlc";
        }
        bundleIdentifiers = @[
            ourIdentifier, // your app
            @"com.spotify.client",
            @"com.apple.iTunes",
            @"com.apple.Music",
            @"com.apple.QuickTimePlayerX",
            @"com.apple.quicktimeplayer",
            @"com.apple.iWork.Keynote",
            @"com.apple.iPhoto",
            @"org.videolan.vlc",
            @"com.apple.Aperture",
            @"com.plexsquared.Plex",
            @"com.soundcloud.desktop",
            @"org.niltsh.MPlayerX",
            @"com.ilabs.PandorasHelper",
            @"com.mahasoftware.pandabar",
            @"com.bitcartel.pandorajam",
            @"org.clementine-player.clementine",
            @"fm.last.Last.fm",
            @"fm.last.Scrobbler",
            @"com.beatport.BeatportPro",
            @"com.Timenut.SongKey",
            @"com.macromedia.fireworks", // the tap messes up their mouse input
            @"at.justp.Theremin",
            @"ru.ya.themblsha.YandexMusic",
            @"com.jriver.MediaCenter18",
            @"com.jriver.MediaCenter19",
            @"com.jriver.MediaCenter20",
            @"co.rackit.mate",
            @"com.ttitt.b-music",
            @"com.beardedspice.BeardedSpice",
            @"com.plug.Plug",
            @"com.netease.163music",
        ];
    });

    return bundleIdentifiers;
}

- (void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting
{
    if (_eventPort == NULL)
        return;

    CGEventTapEnable(_eventPort, newSetting);
}


#pragma mark -
#pragma mark Event tap callbacks

static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
{
    @autoreleasepool {
        SPMediaKeyTap *self = (__bridge SPMediaKeyTap *)refcon;

        if (type == kCGEventTapDisabledByTimeout) {
            NSLog(@"VLC SPMediaKeyTap: Media key event tap was disabled by timeout");
            CGEventTapEnable(self->_eventPort, TRUE);
            return event;
        } else if (type == kCGEventTapDisabledByUserInput) {
            // Was disabled manually by -setShouldInterceptMediaKeyEvents:
            return event;
        } else if (type != NX_SYSDEFINED) {
            // If not a system defined event, do nothing.
            return event;
        }

        NSEvent *nsEvent = nil;
        @try {
            nsEvent = [NSEvent eventWithCGEvent:event];
        }
        @catch (NSException * e) {
            NSLog(@"VLC SPMediaKeyTap: Strange CGEventType: %d: %@", type, e);
            assert(0);
            return event;
        }

        if ([nsEvent subtype] != NX_SUBTYPE_AUX_CONTROL_BUTTONS)
            return event;

        int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16);
        if (keyCode != NX_KEYTYPE_PLAY &&
            keyCode != NX_KEYTYPE_FAST &&
            keyCode != NX_KEYTYPE_REWIND &&
            keyCode != NX_KEYTYPE_PREVIOUS &&
            keyCode != NX_KEYTYPE_NEXT)
            return event;

        [self performSelectorOnMainThread:@selector(handleAndReleaseMediaKeyEvent:) withObject:nsEvent waitUntilDone:NO];

        return NULL;
    }
}

- (void)handleAndReleaseMediaKeyEvent:(NSEvent *)event
{
    uint32_t eventData  = [event data1];

    SPKeyCode keyCode   = ((eventData & 0xFFFF0000) >> 16);
    uint16_t eventFlags = ((eventData & 0x0000FFFF));

    SPKeyState keyState = ((eventFlags & 0xFF00) >> 8);
    BOOL keyRepeat      =  (eventFlags & 0x1);

    [_delegate mediaKeyTap:self
          receivedMediaKey:keyCode
                     state:keyState
                    repeat:keyRepeat];
}


#pragma mark -
#pragma mark Task switching callbacks

- (void)mediaKeyAppListChanged
{
    NSAssert([NSThread isMainThread],
        @"mediaKeyAppListChanged must be called on the main thread!");

    #ifdef DEBUG_SPMEDIAKEY_APPLIST
    [self debugPrintAppList];
    #endif

    if ([_mediaKeyAppList count] == 0)
        return;

    NSRunningApplication *thisApp = [NSRunningApplication currentApplication];
    NSRunningApplication *otherApp = [_mediaKeyAppList firstObject];

    BOOL isCurrent = [thisApp isEqual:otherApp];

    [self setShouldInterceptMediaKeyEvents:isCurrent];
}

- (void)frontmostAppChanged:(NSNotification *)notification
{
    NSRunningApplication *app = [notification.userInfo objectForKey:NSWorkspaceApplicationKey]; 
    if (app.bundleIdentifier == nil)
        return;

    if (![[SPMediaKeyTap mediaKeyUserBundleIdentifiers] containsObject:app.bundleIdentifier])
        return;

    [_mediaKeyAppList removeObject:app];
    [_mediaKeyAppList insertObject:app atIndex:0];
    [self mediaKeyAppListChanged];
}

- (void)appTerminated:(NSNotification *)notification
{
    NSRunningApplication *app = [notification.userInfo objectForKey:NSWorkspaceApplicationKey];
    [_mediaKeyAppList removeObject:app];

    [self mediaKeyAppListChanged];
}

#ifdef DEBUG_SPMEDIAKEY_APPLIST
- (void)debugPrintAppList
{
    NSMutableString *list = [NSMutableString stringWithCapacity:255];
    for (NSRunningApplication *app in _mediaKeyAppList) {
        [list appendFormat:@"     - %@\n", app.bundleIdentifier];
    }
    NSLog(@"List: \n%@", list);
}
#endif

@end
