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


#include "allegro5/allegro.h"
#include "allegro5/internal/aintern.h"
#include "allegro5/internal/aintern_events.h"
#include "allegro5/internal/aintern_keyboard.h"
#include "allegro5/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_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=al_malloc(d->capacity*sizeof(HID_ELEMENT));
	}
	if (d->num_elements>=d->capacity) {
		d->capacity*=2;
		d->element=al_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;
	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 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 *_al_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
/* _get_matching_services:
 * get iterator for corresponding services
 */
static IOReturn _get_matching_services(mach_port_t master_port, int usage_page, int usage, io_iterator_t *hid_object_iterator)
{
   CFMutableDictionaryRef class_dictionary = NULL;
   CFNumberRef usage_ref = NULL;
   CFNumberRef usage_page_ref = NULL;

   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(kIOHIDDeviceUsageKey), usage_ref);
   CFDictionarySetValue(class_dictionary, CFSTR(kIOHIDDeviceUsagePageKey), usage_page_ref);
   }
   IOReturn result = IOServiceGetMatchingServices(master_port, class_dictionary, hid_object_iterator);
   if (usage_ref) CFRelease(usage_ref);
   if (usage_page_ref) CFRelease(usage_page_ref);
   return result;
}

/* _al_osx_hid_scan:
* Scan the hid manager for devices of type 'type',
* and append to the collection col
*/
HID_DEVICE_COLLECTION *_al_osx_hid_scan(int type, HID_DEVICE_COLLECTION* col)
{
	ASSERT(col);
	HID_DEVICE *this_device;
	mach_port_t master_port = 0;
	io_iterator_t hid_object_iterator = 0;
	io_object_t hid_device = 0;
	int usage, usage_page;
	CFTypeRef type_ref;
	CFMutableDictionaryRef properties = NULL, usb_properties = NULL;
	IOCFPlugInInterface **plugin_interface = NULL;
	IOReturn result;
	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) {
		result = _get_matching_services(master_port, usage_page, usage, &hid_object_iterator);
		if ((type == HID_MOUSE) && (hid_object_iterator == IO_OBJECT_NULL)) {
			/* in case of a mouse, GD_Mouse must not be true but can also be a pointing device */
			result = _get_matching_services(master_port, usage_page, kHIDUsage_GD_Pointer, &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))!=0) {
				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)
							al_free(this_device->manufacturer);
						if (this_device->product)
							al_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);
		}
		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=al_malloc(o->capacity*sizeof(HID_DEVICE));
	}
	if (o->count>=o->capacity) {
		o->capacity*=2;
		o->devices=al_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;
}

/* _al_osx_hid_free:
* Release the memory taken up by a collection 
*/
void _al_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)
			al_free(device->manufacturer);
		if (device->product)
			al_free(device->product);
		for (j = 0; j < device->num_elements; j++) {
			element = &device->element[j];
			if (element->name)
				al_free(element->name);
		}
		al_free(device->element);
		if (device->interface) {
			(*(device->interface))->close(device->interface);
			(*(device->interface))->Release(device->interface);
		}
	}
	al_free(col->devices);
}


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