/*         ______   ___    ___
 *        /\  _  \ /\_ \  /\_ \
 *        \ \ \L\ \\//\ \ \//\ \      __     __   _ __   ___
 *         \ \  __ \ \ \ \  \ \ \   /'__`\ /'_ `\/\`'__\/ __`\
 *          \ \ \/\ \ \_\ \_ \_\ \_/\  __//\ \L\ \ \ \//\ \L\ \
 *           \ \_\ \_\/\____\/\____\ \____\ \____ \ \_\\ \____/
 *            \/_/\/_/\/____/\/____/\/____/\/___L\ \/_/ \/___/
 *                                           /\____/
 *                                           \_/__/
 *
 *      MacOS X HID Manager access routines.
 *
 *      By Angelo Mottola.
 *
 *      See readme.txt for copyright information.
 */


#include "allegro.h"
#include "allegro/internal/aintern.h"
#include "allegro/platform/aintosx.h"

#ifndef ALLEGRO_MACOSX
#error Something is wrong with the makefile
#endif

#define USAGE(p, u) (((p)<<16)+(u))

static void hid_store_element_data(CFTypeRef element, int type, HID_DEVICE *device, int app, int collection, int index);
static void hid_scan_element(CFTypeRef element, HID_DEVICE *device);
static void hid_scan_physical_collection(CFTypeRef properties, HID_DEVICE *device, int app, int collection);
static void hid_scan_application_collection(CFMutableDictionaryRef properties, HID_DEVICE *device);

static HID_DEVICE* add_device(HID_DEVICE_COLLECTION*);

/* add_element:
 * Add a new, blank element to a device.
 * This is later specialized into a button
 * axis or whatever 
 */
static HID_ELEMENT* add_element(HID_DEVICE* d) {
   HID_ELEMENT* e;
   if (d->element==NULL) {
      d->capacity=8;
      d->element=malloc(d->capacity*sizeof(HID_ELEMENT));
   }
   if (d->num_elements>=d->capacity) {
      d->capacity*=2;
      d->element=realloc(d->element, d->capacity*sizeof(HID_ELEMENT));
   }
   e=&d->element[d->num_elements++];
   memset(e, 0, sizeof(HID_ELEMENT));
   return e;
}

/* i_val:
 * Helper method - get an integer value from a dictionary
 * Defaults to 0 if the value is not found; in practice this 
 * should not occur.
 */
static int i_val(CFTypeRef d, CFStringRef key) {
   int ans;
   CFTypeRef num = CFDictionaryGetValue(d, key);
   if (num) 
      CFNumberGetValue(num, kCFNumberIntType, &ans);
   else
      ans = 0;
   return ans;
}

/* hid_store_element_data:
 * Parse this HID element, break it down and store the 
 * relevant data in the device structure
 */
static void hid_store_element_data(CFTypeRef element, int type, HID_DEVICE *device, int app, int col, int idx)
{
   HID_ELEMENT *hid_element;
   CFTypeRef type_ref;
   AL_CONST char *name;

   hid_element = add_element(device);
   hid_element->type = type;
   hid_element->index = idx;
   hid_element->col = col;
   hid_element->app = app;
   hid_element->cookie = (IOHIDElementCookie) i_val(element, CFSTR(kIOHIDElementCookieKey));
   hid_element->min = i_val(element, CFSTR(kIOHIDElementMinKey));
   hid_element->max = i_val(element, CFSTR(kIOHIDElementMaxKey));
   type_ref = CFDictionaryGetValue(element, CFSTR(kIOHIDElementNameKey));
   if ((type_ref) && (name = CFStringGetCStringPtr(type_ref, CFStringGetSystemEncoding())))
      hid_element->name = strdup(name);
   else
      hid_element->name = NULL;
}

/* hid_scan_application:
 * scan the elements that make up one 'application'
 * i.e. one unit like a joystick.
 */
static void hid_scan_application(CFTypeRef properties, HID_DEVICE *device, int app)
{
   CFTypeRef array_ref, element;
   int type, usage_page, usage;
   int i;
   int axis=0;
   int stick=0;
   array_ref = CFDictionaryGetValue(properties, CFSTR(kIOHIDElementKey));
   if ((array_ref) && (CFGetTypeID(array_ref) == CFArrayGetTypeID())) {
      for (i = 0; i < CFArrayGetCount(array_ref); i++) {
         element = CFArrayGetValueAtIndex(array_ref, i);
         if (CFGetTypeID(element) == CFDictionaryGetTypeID()) {
            type = i_val(element, CFSTR(kIOHIDElementTypeKey));
            usage = i_val(element, CFSTR(kIOHIDElementUsageKey));
            usage_page = i_val(element, CFSTR(kIOHIDElementUsagePageKey));
            if (type == kIOHIDElementTypeCollection) 
               {
                  /* It is a collection; recurse into it, if it is
                   part of the generic desktop (sometimes the whole
                   joystick is wrapped up inside a collection like
                   this. */
                  if (usage_page == kHIDPage_GenericDesktop)
                     hid_scan_application(element, device, app);                     
               }
            else
               { 
                  switch (usage_page) {
                  case kHIDPage_GenericDesktop:
                     switch(usage) {
                     case kHIDUsage_GD_Pointer:
                        if (axis!=0) {
                           /* already have some elements in this stick */
                           ++stick;
                           axis=0;
                        }
                        hid_scan_physical_collection(element, device, app, stick);
                        ++stick;
                        break;
                     case kHIDUsage_GD_X:
                        hid_store_element_data(element, HID_ELEMENT_AXIS_PRIMARY_X, device, app, stick, axis++);
                        break;
                        
                     case kHIDUsage_GD_Y:
                        hid_store_element_data(element, HID_ELEMENT_AXIS_PRIMARY_Y, device, app, stick, axis++);
                        break;
                        
                     case kHIDUsage_GD_Z:
                     case kHIDUsage_GD_Rx:
                     case kHIDUsage_GD_Ry:
                     case kHIDUsage_GD_Rz:
                        hid_store_element_data(element, HID_ELEMENT_AXIS, device,app, stick, axis++);
                        break;
                        
                     case kHIDUsage_GD_Slider:
                     case kHIDUsage_GD_Dial:
                     case kHIDUsage_GD_Wheel:
                        /* If we've already seen some axes on this stick, move to the next one */
                        if (axis > 0) 
                           {
                              ++stick;
                              axis=0;
                           }
                        hid_store_element_data(element, HID_ELEMENT_STANDALONE_AXIS, device, app, stick++, 0);
                        break;
                    
                     case kHIDUsage_GD_Hatswitch:
                        /* If we've already seen some axes on this stick, move to the next one */
                        if (axis > 0) 
                           {
                              ++stick;
                              axis=0;
                           }
                        hid_store_element_data(element, HID_ELEMENT_HAT, device, app, stick++, 0);
                        break;
                     }
                     break;
                  case kHIDPage_Button:
                     hid_store_element_data(element, HID_ELEMENT_BUTTON, device, app, 0, usage-1);
                     break;
                  }
               }
            if (axis>=2) {
               ++stick;
               axis=0;
            }
         }
      }
   }
}

/* hid_scan_physical_collection:
 * scan the elements that make up one 'stick'
 */
static void hid_scan_physical_collection(CFTypeRef properties, HID_DEVICE *device, int app, int stick)
{
   CFTypeRef array_ref, element;
   int type, usage_page, usage;
   int i;
   int axis=0;
   array_ref = CFDictionaryGetValue(properties, CFSTR(kIOHIDElementKey));
   if ((array_ref) && (CFGetTypeID(array_ref) == CFArrayGetTypeID())) {
      for (i = 0; i < CFArrayGetCount(array_ref); i++) {
         element = CFArrayGetValueAtIndex(array_ref, i);
         if (CFGetTypeID(element) == CFDictionaryGetTypeID()) {
            type = i_val(element, CFSTR(kIOHIDElementTypeKey));
            usage = i_val(element, CFSTR(kIOHIDElementUsageKey));
            usage_page = i_val(element, CFSTR(kIOHIDElementUsagePageKey));
            switch (usage_page) {
            case kHIDPage_GenericDesktop:
               switch(usage) {
               case kHIDUsage_GD_X:
                  hid_store_element_data(element, HID_ELEMENT_AXIS_PRIMARY_X, device, app, stick, axis++);
                  break;
                
               case kHIDUsage_GD_Y:
                  hid_store_element_data(element, HID_ELEMENT_AXIS_PRIMARY_Y, device, app, stick, axis++);
                  break;
                
               case kHIDUsage_GD_Z:
               case kHIDUsage_GD_Rx:
               case kHIDUsage_GD_Ry:
               case kHIDUsage_GD_Rz:
                  hid_store_element_data(element, HID_ELEMENT_AXIS, device,app, stick, axis++);
                  break;
               }
               break;
            case kHIDPage_Button:
               hid_store_element_data(element, HID_ELEMENT_BUTTON, device, app, 0, usage-1);
               break;
            }
         }
      }
   }
}

/* hid_scan_application_collection:
 * Scan the elements array; each element will be an 'application'
 * i.e. one joystick, gamepad or mouse
 *
 */
static void hid_scan_application_collection(CFMutableDictionaryRef properties, HID_DEVICE *device)
{
   CFTypeRef array_ref, element;

   int type, usage, usage_page;
   int i;

   array_ref = CFDictionaryGetValue(properties, CFSTR(kIOHIDElementKey));
   if ((array_ref) && (CFGetTypeID(array_ref) == CFArrayGetTypeID())) {
      for (i = 0; i < CFArrayGetCount(array_ref); i++) {
         element = CFArrayGetValueAtIndex(array_ref, i);
         if (CFGetTypeID(element) == CFDictionaryGetTypeID()) {
            usage=i_val(element, CFSTR(kIOHIDElementUsageKey));
            usage_page=i_val(element, CFSTR(kIOHIDElementUsagePageKey));
            switch(USAGE(usage_page,  usage)) {
            case USAGE(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick):
               device->type=HID_JOYSTICK;
               hid_scan_application(element, device, device->cur_app);
               device->cur_app++;
               break;
            case USAGE(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad):
               device->type=HID_GAMEPAD;
               hid_scan_application(element, device, device->cur_app);
               device->cur_app++;
               break;
            case USAGE(kHIDPage_GenericDesktop, kHIDUsage_GD_Mouse):
               device->type=HID_MOUSE;
               hid_scan_application(element, device, device->cur_app);
               device->cur_app++;
               break;
            }
         }
      }
   }
}

/* get_usb_properties:
 * Get a property dictionary from the USB plane. 
 */
static CFMutableDictionaryRef get_usb_properties(io_object_t device)
{
   io_registry_entry_t parent, grandparent;
   CFMutableDictionaryRef usb_properties = NULL;
   if (IORegistryEntryGetParentEntry(device, kIOServicePlane, &parent) == KERN_SUCCESS) {
      if (IORegistryEntryGetParentEntry(parent, kIOServicePlane, &grandparent) == KERN_SUCCESS) {
         IORegistryEntryCreateCFProperties (grandparent, &usb_properties, 
                                            kCFAllocatorDefault, kNilOptions);
         IOObjectRelease(grandparent);
      }
      IOObjectRelease(parent);
   }
   return usb_properties;
}

#if OSX_HID_PSEUDO_SCAN
/*
 * Pseudo scan - for development purposes, if someone has hardware
 * that isn't parsed by this code, you can ask them to dump _their_ scan
 * as a plist, then reload it here in order to debug it.
*/
HID_DEVICE_COLLECTION *osx_hid_scan(int type, HID_DEVICE_COLLECTION* col)
{
   HID_DEVICE* this_device;
   NSDictionary* properties = nil;
   NSString* filename;
   switch (type) {
   case HID_GAMEPAD:
      filename = @"gamepad.plist";
      break;
   case HID_JOYSTICK:
      filename = @"joystick.plist";
      break;
   case HID_MOUSE:
      filename = @"mouse.plist";
      break;
   default:
      filename = nil;
      break;
   }
   if (filename != nil)
      properties = [NSDictionary dictionaryWithContentsOfFile:filename];
   if (properties)
   {   
      this_device = add_device(col);
      this_device->manufacturer = strdup([((NSString*) [properties objectForKey: @kIOHIDManufacturerKey]) lossyCString]);
      this_device->product = strdup([((NSString*) [properties objectForKey: @kIOHIDProductKey]) lossyCString]);
      hid_scan_application_collection((CFDictionaryRef) properties, this_device);
      [properties release];
   }
   return col;
}
#else
/* osx_hid_scan:
 * Scan the hid manager for devices of type 'type',
 * and append to the collection col
 */
HID_DEVICE_COLLECTION *osx_hid_scan(int type, HID_DEVICE_COLLECTION* col)
{
   ASSERT(col);
   HID_DEVICE *device, *this_device;
   mach_port_t master_port = 0;
   io_iterator_t hid_object_iterator = 0;
   io_object_t hid_device = 0;
   CFMutableDictionaryRef class_dictionary = NULL;
   int usage, usage_page;
   int app_count;
   CFTypeRef type_ref;
   CFNumberRef usage_ref = NULL, usage_page_ref = NULL;
   CFMutableDictionaryRef properties = NULL, usb_properties = NULL;
   IOCFPlugInInterface **plugin_interface = NULL;
   IOReturn result;
   AL_CONST char *string;
   SInt32 score = 0;
   int error;
   
   usage_page = kHIDPage_GenericDesktop;
   switch (type) {
   case HID_MOUSE:
      usage = kHIDUsage_GD_Mouse;
      break;
   case HID_JOYSTICK:
      usage = kHIDUsage_GD_Joystick;
      break;
   case HID_GAMEPAD:
      usage=kHIDUsage_GD_GamePad;
      break;
   }

   result = IOMasterPort(bootstrap_port, &master_port);
   if (result == kIOReturnSuccess) {
      class_dictionary = IOServiceMatching(kIOHIDDeviceKey);
      if (class_dictionary) {
         /* Add key for device type to refine the matching dictionary. */
         usage_ref = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage);
         usage_page_ref = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage_page);
         CFDictionarySetValue(class_dictionary, CFSTR(kIOHIDPrimaryUsageKey), usage_ref);
         CFDictionarySetValue(class_dictionary, CFSTR(kIOHIDPrimaryUsagePageKey), usage_page_ref);
      }
      result = IOServiceGetMatchingServices(master_port, class_dictionary, &hid_object_iterator);
      if ((result == kIOReturnSuccess) && (hid_object_iterator)) {
         /* Ok, we have a list of attached HID devices; scan them. */
         while ((hid_device = IOIteratorNext(hid_object_iterator))!=NULL) {
            if ((IORegistryEntryCreateCFProperties(hid_device, &properties, kCFAllocatorDefault, kNilOptions) == KERN_SUCCESS) && (properties != NULL)) {
               error = FALSE;
               this_device = add_device(col);
               this_device->type = type;
               if (col->count==0) {
                  this_device->cur_app=0;
               }
               else {
                  this_device->cur_app=col->devices[col->count-1].cur_app;
               }
               /* 
                * Mac OS X currently is not mirroring all USB properties
                * to HID page so need to look at USB device page also. 
                */
               usb_properties = get_usb_properties(hid_device);
               type_ref = CFDictionaryGetValue(properties, CFSTR(kIOHIDManufacturerKey));
               if (!type_ref)
                  type_ref = CFDictionaryGetValue(usb_properties, CFSTR("USB Vendor Name"));
               if ((type_ref) && (string = CFStringGetCStringPtr(type_ref, CFStringGetSystemEncoding())))
                  this_device->manufacturer = strdup(string);
               else
                  this_device->manufacturer = NULL;
               type_ref = CFDictionaryGetValue(properties, CFSTR(kIOHIDProductKey));
               if (!type_ref)
                  type_ref = CFDictionaryGetValue(usb_properties, CFSTR("USB Product Name"));
               if ((type_ref) && (string = CFStringGetCStringPtr(type_ref, CFStringGetSystemEncoding())))
                  this_device->product = strdup(string);
               else
                  this_device->product = NULL;

               type_ref = CFDictionaryGetValue(usb_properties, CFSTR("USB Address"));
               if ((type == HID_MOUSE) && (!type_ref)) {
                  /* Not an USB mouse. Must be integrated trackpad: we report it as a single button mouse */
                  add_element(this_device)->type = HID_ELEMENT_BUTTON;
               }
               else {
                  /* Scan for device elements */
                  this_device->num_elements = 0;
                  hid_scan_application_collection(properties, this_device);
               }

               this_device->interface = NULL;
               if ((type == HID_JOYSTICK) || (type == HID_GAMEPAD)) {
                  /* Joystick or gamepad device: create HID interface */
                  if (IOCreatePlugInInterfaceForService(hid_device, kIOHIDDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plugin_interface, &score) != kIOReturnSuccess)
                     error = TRUE;
                  else {
                     if ((*plugin_interface)->QueryInterface(plugin_interface, CFUUIDGetUUIDBytes(kIOHIDDeviceInterfaceID), (void *)&(this_device->interface)) != S_OK)
                        error = TRUE;
                     (*plugin_interface)->Release(plugin_interface);
                     if ((*(this_device->interface))->open(this_device->interface, 0) != KERN_SUCCESS)
                        error = TRUE;
                  }
               }
               if (error) {
                  if (this_device->manufacturer)
                     free(this_device->manufacturer);
                  if (this_device->product)
                     free(this_device->product);
                  if (this_device->interface)
                     (*(this_device->interface))->Release(this_device->interface);
                  this_device->interface=NULL;
                  --col->count;
               }

               CFRelease(properties);
               CFRelease(usb_properties);
            }
         }
         IOObjectRelease(hid_object_iterator);
      }
      CFRelease(usage_ref);
      CFRelease(usage_page_ref);
      mach_port_deallocate(mach_task_self(), master_port);
   }

   return col;
}
#endif
/* add_device:
 * add a new device to a collection
 */
static HID_DEVICE* add_device(HID_DEVICE_COLLECTION* o) {
   HID_DEVICE* d;
   if (o->devices==NULL) {
      o->count=0;
      o->capacity=1;
      o->devices=malloc(o->capacity*sizeof(HID_DEVICE));
   }
   if (o->count>=o->capacity) {
      o->capacity*=2;
      o->devices=realloc(o->devices, o->capacity*sizeof(HID_DEVICE));
   }
   d=&o->devices[o->count];
   memset(d, 0, sizeof(HID_DEVICE));
   /* Chain onto the preceding app count */
   if (o->count>0)
      d->cur_app = o->devices[o->count-1].cur_app;
   ++o->count;
   return d;
}

/* osx_hid_free:
 * Release the memory taken up by a collection 
 */
void osx_hid_free(HID_DEVICE_COLLECTION *col)
{
   HID_DEVICE *device;
   HID_ELEMENT *element;
   int i, j;

   for (i = 0; i < col->count; i++) {
      device = &col->devices[i];
      if (device->manufacturer)
         free(device->manufacturer);
      if (device->product)
         free(device->product);
      for (j = 0; j < device->num_elements; j++) {
         element = &device->element[j];
         if (element->name)
            free(element->name);
      }
      free(device->element);
      if (device->interface) {
         (*(device->interface))->close(device->interface);
         (*(device->interface))->Release(device->interface);
      }
   }
   free(col->devices);
}

/* Local variables:       */
/* c-basic-offset: 3      */
/* indent-tabs-mode: nil  */
/* End:                   */
