/* -*- Mode: ObjC; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*****************************************************************************
 * HIDRemoteControlDevice.m
 * RemoteControlWrapper
 *
 * Created by Martin Kahr on 11.03.06 under a MIT-style license.
 * Copyright (c) 2006 martinkahr.com. All rights reserved.
 *
 * Code modified and adapted to OpenOffice.org
 * by Eric Bachard on 11.08.2008 under the same license
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 *****************************************************************************/

#import "HIDRemoteControlDevice.h"

#import <mach/mach.h>
#import <mach/mach_error.h>
#import <IOKit/IOKitLib.h>
#import <IOKit/IOCFPlugIn.h>
#import <IOKit/hid/IOHIDKeys.h>
#import <Carbon/Carbon.h>

@interface HIDRemoteControlDevice (PrivateMethods)
- (NSDictionary*) cookieToButtonMapping; // Creates the dictionary using the magics, depending on the remote
- (IOHIDQueueInterface**) queue;
- (IOHIDDeviceInterface**) hidDeviceInterface;
- (void) handleEventWithCookieString: (NSString*) cookieString sumOfValues: (SInt32) sumOfValues;
- (void) removeNotificationObserver;
- (void) remoteControlAvailable:(NSNotification *)notification;

@end

@interface HIDRemoteControlDevice (IOKitMethods)
+ (io_object_t) findRemoteDevice;
- (IOHIDDeviceInterface**) createInterfaceForDevice: (io_object_t) hidDevice;
- (BOOL) initializeCookies;
- (BOOL) openDevice;
@end

@implementation HIDRemoteControlDevice

+ (const char*) remoteControlDeviceName {
    return "";
}

+ (BOOL) isRemoteAvailable {
    io_object_t hidDevice = [self findRemoteDevice];
    if (hidDevice != 0) {
        IOObjectRelease(hidDevice);
        return YES;
    } else {
        return NO;
    }
}

- (id) initWithDelegate: (id) _remoteControlDelegate {
    if ([[self class] isRemoteAvailable] == NO) return nil;

    if ( (self = [super initWithDelegate: _remoteControlDelegate]) ) {
        openInExclusiveMode = YES;
        queue = NULL;
        hidDeviceInterface = NULL;
        cookieToButtonMapping = [[NSMutableDictionary alloc] init];

        [self setCookieMappingInDictionary: cookieToButtonMapping];

        NSEnumerator* enumerator = [cookieToButtonMapping objectEnumerator];
        NSNumber* identifier;
        supportedButtonEvents = 0;
        while( (identifier = [enumerator nextObject]) ) {
            supportedButtonEvents |= [identifier intValue];
        }

        fixSecureEventInputBug = [[NSUserDefaults standardUserDefaults] boolForKey: @"remoteControlWrapperFixSecureEventInputBug"];
    }

    return self;
}

- (void) dealloc {
    [self removeNotificationObserver];
    [self stopListening:self];
    [cookieToButtonMapping release];
    [super dealloc];
}

- (void) sendRemoteButtonEvent: (RemoteControlEventIdentifier) event pressedDown: (BOOL) pressedDown {
    [delegate sendRemoteButtonEvent: event pressedDown: pressedDown remoteControl:self];
}

- (void) setCookieMappingInDictionary: (NSMutableDictionary*) cookieToButtonMap {
    (void)cookieToButtonMap;
}

- (int) remoteIdSwitchCookie {
    return 0;
}

- (BOOL) sendsEventForButtonIdentifier: (RemoteControlEventIdentifier) identifier {
    return (supportedButtonEvents & identifier) == identifier;
}

- (BOOL) isListeningToRemote {
    return (hidDeviceInterface != NULL && allCookies != NULL && queue != NULL);
}

- (void) setListeningToRemote: (BOOL) value {
    if (value == NO) {
        [self stopListening:self];
    } else {
        [self startListening:self];
    }
}

- (BOOL) isOpenInExclusiveMode {
    return openInExclusiveMode;
}
- (void) setOpenInExclusiveMode: (BOOL) value {
    openInExclusiveMode = value;
}

- (BOOL) processesBacklog {
    return processesBacklog;
}
- (void) setProcessesBacklog: (BOOL) value {
    processesBacklog = value;
}

- (void) startListening: (id) sender {
    (void)sender;
    if ([self isListeningToRemote]) return;

    // 4th July 2007

    // A security update in february of 2007 introduced an odd behavior.
    // Whenever SecureEventInput is activated or deactivated the exclusive access
    // to the remote control device is lost. This leads to very strange behavior where
    // a press on the Menu button activates FrontRow while your app still gets the event.
    // A great number of people have complained about this.

    // Enabling the SecureEventInput and keeping it enabled does the trick.

    // I'm pretty sure this is a kind of bug at Apple and I'm in contact with the responsible
    // Apple Engineer. This solution is not a perfect one - I know.
    // One of the side effects is that applications that listen for special global keyboard shortcuts (like Quicksilver)
    // may get into problems as they no longer get the events.
    // As there is no official Apple Remote API from Apple I also failed to open a technical incident on this.

    // Note that there is a corresponding DisableSecureEventInput in the stopListening method below.

    if ([self isOpenInExclusiveMode] && fixSecureEventInputBug) EnableSecureEventInput();

    [self removeNotificationObserver];

    io_object_t hidDevice = [[self class] findRemoteDevice];
    if (hidDevice == 0) return;

    if ([self createInterfaceForDevice:hidDevice] == NULL) {
        goto error;
    }

    if ([self initializeCookies]==NO) {
        goto error;
    }

    if ([self openDevice]==NO) {
        goto error;
    }
    // be KVO friendly
    [self willChangeValueForKey:@"listeningToRemote"];
    [self didChangeValueForKey:@"listeningToRemote"];
    goto cleanup;

error:
    [self stopListening:self];
    DisableSecureEventInput();

cleanup:
    IOObjectRelease(hidDevice);
}

- (void) stopListening: (id) sender {
    (void)sender;
    if ([self isListeningToRemote]==NO) return;

    BOOL sendNotification = NO;

    if (eventSource != NULL) {
        CFRunLoopRemoveSource(CFRunLoopGetCurrent(), eventSource, kCFRunLoopDefaultMode);
        CFRelease(eventSource);
        eventSource = NULL;
    }
    if (queue != NULL) {
        (*queue)->stop(queue);

        //dispose of queue
        (*queue)->dispose(queue);

        //release the queue we allocated
        (*queue)->Release(queue);

        queue = NULL;

        sendNotification = YES;
    }

    if (allCookies != nil) {
        [allCookies autorelease];
        allCookies = nil;
    }

    if (hidDeviceInterface != NULL) {
        //close the device
        (*hidDeviceInterface)->close(hidDeviceInterface);

        //release the interface
        (*hidDeviceInterface)->Release(hidDeviceInterface);

        hidDeviceInterface = NULL;
    }

    if ([self isOpenInExclusiveMode] && fixSecureEventInputBug) DisableSecureEventInput();

    if ([self isOpenInExclusiveMode] && sendNotification) {
        [[self class] sendFinishedNotificationForAppIdentifier: nil];
    }
    // be KVO friendly
    [self willChangeValueForKey:@"listeningToRemote"];
    [self didChangeValueForKey:@"listeningToRemote"];
}

@end

@implementation HIDRemoteControlDevice (PrivateMethods)

- (IOHIDQueueInterface**) queue {
    return queue;
}

- (IOHIDDeviceInterface**) hidDeviceInterface {
    return hidDeviceInterface;
}


- (NSDictionary*) cookieToButtonMapping {
    return cookieToButtonMapping;
}

- (NSString*) validCookieSubstring: (NSString*) cookieString {
    if (cookieString == nil || [cookieString length] == 0) return nil;
    NSEnumerator* keyEnum = [[self cookieToButtonMapping] keyEnumerator];
    NSString* key;
    while( (key = [keyEnum nextObject]) ) {
        NSRange range = [cookieString rangeOfString:key];
        if (range.location == 0) return key;
    }
    return nil;
}

- (void) handleEventWithCookieString: (NSString*) cookieString sumOfValues: (SInt32) sumOfValues {
    /*
    if (previousRemainingCookieString) {
        cookieString = [previousRemainingCookieString stringByAppendingString: cookieString];
        NSLog( @"Apple Remote: New cookie string is %@", cookieString);
        [previousRemainingCookieString release], previousRemainingCookieString=nil;
    }*/
    if (cookieString == nil || [cookieString length] == 0) return;

    NSNumber* buttonId = [[self cookieToButtonMapping] objectForKey: cookieString];
    if (buttonId != nil) {
       switch ( [buttonId intValue] )
       {
       case kMetallicRemote2009ButtonPlay:
       case kMetallicRemote2009ButtonMiddlePlay:
           buttonId = [NSNumber numberWithInt:kRemoteButtonPlay];
           break;
       default:
           break;
       }
       [self sendRemoteButtonEvent: [buttonId intValue] pressedDown: (sumOfValues>0)];

    } else {
        // let's see if a number of events are stored in the cookie string. this does
        // happen when the main thread is too busy to handle all incoming events in time.
        NSString* subCookieString;
        NSString* lastSubCookieString=nil;
        while( (subCookieString = [self validCookieSubstring: cookieString]) ) {
            cookieString = [cookieString substringFromIndex: [subCookieString length]];
            lastSubCookieString = subCookieString;
            if (processesBacklog) [self handleEventWithCookieString: subCookieString sumOfValues:sumOfValues];
        }
        if (processesBacklog == NO && lastSubCookieString != nil) {
            // process the last event of the backlog and assume that the button is not pressed down any longer.
            // The events in the backlog do not seem to be in order and therefore (in rare cases) the last event might be
            // a button pressed down event while in reality the user has released it.
            // NSLog(@"processing last event of backlog");
            [self handleEventWithCookieString: lastSubCookieString sumOfValues:0];
        }
        if ([cookieString length] > 0) {
                NSLog( @"Apple Remote: Unknown button for cookiestring %@", cookieString);
        }
    }
}

- (void) removeNotificationObserver {
    [[NSDistributedNotificationCenter defaultCenter] removeObserver:self name:FINISHED_USING_REMOTE_CONTROL_NOTIFICATION object:nil];
}

- (void) remoteControlAvailable:(NSNotification *)notification {
    (void)notification;
    [self removeNotificationObserver];
    [self startListening: self];
}

@end

/*  Callback method for the device queue
Will be called for any event of any type (cookie) to which we subscribe
*/
static void QueueCallbackFunction(void* target,  IOReturn result, void* refcon, void* sender) {
    (void)refcon;
    (void)sender;
    if ((intptr_t)target < 0) {
        NSLog( @"Apple Remote: QueueCallbackFunction called with invalid target!");
        return;
    }
    NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];

    HIDRemoteControlDevice* remote = (HIDRemoteControlDevice*)target;
    IOHIDEventStruct event;
    AbsoluteTime const zeroTime = {0,0};
    NSMutableString* cookieString = [NSMutableString string];
    SInt32           sumOfValues = 0;
    while (result == kIOReturnSuccess)
    {
        result = (*[remote queue])->getNextEvent([remote queue], &event, zeroTime, 0);
        if ( result != kIOReturnSuccess )
            continue;

        //printf("%d %d %d\n", event.elementCookie, event.value, event.longValue);

        if (((int)event.elementCookie)!=5) {
            sumOfValues+=event.value;
            [cookieString appendString:[NSString stringWithFormat:@"%lld_", (long long) (intptr_t) event.elementCookie]];
        }
    }
    [remote handleEventWithCookieString: cookieString sumOfValues: sumOfValues];

    [pool release];
}

@implementation HIDRemoteControlDevice (IOKitMethods)

- (IOHIDDeviceInterface**) createInterfaceForDevice: (io_object_t) hidDevice {
    io_name_t               className;
    IOCFPlugInInterface**   plugInInterface = NULL;
    HRESULT                 plugInResult = S_OK;
    SInt32                  score = 0;
    IOReturn                ioReturnValue = kIOReturnSuccess;

    hidDeviceInterface = NULL;

    ioReturnValue = IOObjectGetClass(hidDevice, className);

    if (ioReturnValue != kIOReturnSuccess) {
        NSLog( @"Apple Remote: Error: Failed to get RemoteControlDevice class name.");
        return NULL;
    }

    ioReturnValue = IOCreatePlugInInterfaceForService(hidDevice,
                                                      kIOHIDDeviceUserClientTypeID,
                                                      kIOCFPlugInInterfaceID,
                                                      &plugInInterface,
                                                      &score);
    if (ioReturnValue == kIOReturnSuccess)
    {
        //Call a method of the intermediate plug-in to create the device interface
        plugInResult = (*plugInInterface)->QueryInterface(plugInInterface, CFUUIDGetUUIDBytes(kIOHIDDeviceInterfaceID), (LPVOID) &hidDeviceInterface);

        if (plugInResult != S_OK) {
            NSLog( @"Apple Remote: Error: Couldn't create HID class device interface");
        }
        // Release
        if (plugInInterface) (*plugInInterface)->Release(plugInInterface);
    }
    return hidDeviceInterface;
}

- (BOOL) initializeCookies {
    IOHIDDeviceInterface122** handle = (IOHIDDeviceInterface122**)hidDeviceInterface;
    IOHIDElementCookie      cookie;
    id                      object;
    NSArray*                elements = nil;
    NSDictionary*           element;
    IOReturn success;

    if (!handle || !(*handle)) return NO;

    // Copy all elements, since we're grabbing most of the elements
    // for this device anyway, and thus, it's faster to iterate them
    // ourselves. When grabbing only one or two elements, a matching
    // dictionary should be passed in here instead of NULL.
    success = (*handle)->copyMatchingElements(handle, NULL, (CFArrayRef*)&elements);

    if (success == kIOReturnSuccess) {

        /*
        cookies = calloc(NUMBER_OF_APPLE_REMOTE_ACTIONS, sizeof(IOHIDElementCookie));
        memset(cookies, 0, sizeof(IOHIDElementCookie) * NUMBER_OF_APPLE_REMOTE_ACTIONS);
        */
        allCookies = [[NSMutableArray alloc] init];

        NSEnumerator *elementsEnumerator = [elements objectEnumerator];

        while ( (element = [elementsEnumerator nextObject]) ) {
            //Get cookie
            object = [element valueForKey: (NSString*)CFSTR(kIOHIDElementCookieKey) ];
            if (object == nil || ![object isKindOfClass:[NSNumber class]]) continue;
            if (object == NULL || CFGetTypeID(object) != CFNumberGetTypeID()) continue;
            cookie = (IOHIDElementCookie) [object longValue];

            [allCookies addObject: [NSNumber numberWithInt:(int)cookie]];
        }
        CFRelease(elements);
        elements=nil;
    } else {
        return NO;
    }

    return YES;
}

- (BOOL) openDevice {
    IOHIDOptionsType openMode = kIOHIDOptionsTypeNone;
    if ([self isOpenInExclusiveMode]) openMode = kIOHIDOptionsTypeSeizeDevice;
    IOReturn ioReturnValue = (*hidDeviceInterface)->open(hidDeviceInterface, openMode);

    if (ioReturnValue == KERN_SUCCESS) {
        queue = (*hidDeviceInterface)->allocQueue(hidDeviceInterface);
        if (queue) {
            (*queue)->create(queue, 0, 12);    //depth: maximum number of elements in queue before oldest elements in queue begin to be lost.

            IOHIDElementCookie cookie;
            NSEnumerator *allCookiesEnumerator = [allCookies objectEnumerator];

            while ( (cookie = (IOHIDElementCookie)[[allCookiesEnumerator nextObject] intValue]) ) {
                (*queue)->addElement(queue, cookie, 0);
            }

            // add callback for async events
            ioReturnValue = (*queue)->createAsyncEventSource(queue, &eventSource);
            if (ioReturnValue == KERN_SUCCESS) {
                ioReturnValue = (*queue)->setEventCallout(queue,QueueCallbackFunction, self, NULL);
                if (ioReturnValue == KERN_SUCCESS) {
                    CFRunLoopAddSource(CFRunLoopGetCurrent(), eventSource, kCFRunLoopDefaultMode);

                    //start data delivery to queue
                    (*queue)->start(queue);
                    return YES;
                } else {
                    NSLog( @"Apple Remote: Error when setting event callback");
                }
            } else {
                NSLog( @"Apple Remote: Error when creating async event source");
            }
        } else {
            NSLog( @"Apple Remote: Error when opening device");
        }
    } else if (ioReturnValue == kIOReturnExclusiveAccess) {
        // the device is used exclusive by another application

        // 1. we register for the FINISHED_USING_REMOTE_CONTROL_NOTIFICATION notification
        [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(remoteControlAvailable:) name:FINISHED_USING_REMOTE_CONTROL_NOTIFICATION object:nil];

        // 2. send a distributed notification that we wanted to use the remote control
        [[self class] sendRequestForRemoteControlNotification];
    }
    return NO;
}

+ (io_object_t) findRemoteDevice {
    CFMutableDictionaryRef hidMatchDictionary = NULL;
    IOReturn ioReturnValue = kIOReturnSuccess;
    io_iterator_t hidObjectIterator = 0;
    io_object_t hidDevice = 0;

    // Set up a matching dictionary to search the I/O Registry by class
    // name for all HID class devices
    hidMatchDictionary = IOServiceMatching([self remoteControlDeviceName]);

    // Now search I/O Registry for matching devices.
    SAL_WNODEPRECATED_DECLARATIONS_PUSH // kIOMasterPortDefault (12.0)
    ioReturnValue = IOServiceGetMatchingServices(kIOMasterPortDefault, hidMatchDictionary, &hidObjectIterator);
    SAL_WNODEPRECATED_DECLARATIONS_POP

    if ((ioReturnValue == kIOReturnSuccess) && (hidObjectIterator != 0)) {
        hidDevice = IOIteratorNext(hidObjectIterator);
    }

    // release the iterator
    IOObjectRelease(hidObjectIterator);

    return hidDevice;
}

@end

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
