/*
 ** i_joystick.cpp
 **
 **---------------------------------------------------------------------------
 ** Copyright 2012-2015 Alexey Lysiuk
 ** All rights reserved.
 **
 ** Redistribution and use in source and binary forms, with or without
 ** modification, are permitted provided that the following conditions
 ** are met:
 **
 ** 1. Redistributions of source code must retain the above copyright
 **    notice, this list of conditions and the following disclaimer.
 ** 2. Redistributions in binary form must reproduce the above copyright
 **    notice, this list of conditions and the following disclaimer in the
 **    documentation and/or other materials provided with the distribution.
 ** 3. The name of the author may not be used to endorse or promote products
 **    derived from this software without specific prior written permission.
 **
 ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.
 **---------------------------------------------------------------------------
 **
 */

#include <IOKit/IOCFPlugIn.h>
#include <IOKit/IOMessage.h>
#include <IOKit/hid/IOHIDLib.h>

#include "d_eventbase.h"
#include "i_system.h"
#include "m_argv.h"
#include "m_joy.h"
#include "templates.h"
#include "v_text.h"
#include "printf.h"
#include "keydef.h"


EXTERN_CVAR(Bool, joy_axespolling)


namespace
{

FString ToFString(const CFStringRef string)
{
	if (NULL == string)
	{
		return FString();
	}

	const CFIndex stringLength = CFStringGetLength(string);

	if (0 == stringLength)
	{
		return FString();
	}

	const size_t bufferSize = CFStringGetMaximumSizeForEncoding(stringLength, kCFStringEncodingUTF8) + 1;

	char buffer[bufferSize];
	memset(buffer, 0, bufferSize);

	CFStringGetCString(string, buffer, bufferSize, kCFStringEncodingUTF8);

	return FString(buffer);
}


// ---------------------------------------------------------------------------


class IOKitJoystick : public IJoystickConfig
{
public:
	explicit IOKitJoystick(io_object_t device);
	virtual ~IOKitJoystick();

	virtual FString GetName();
	virtual float GetSensitivity();
	virtual void SetSensitivity(float scale);

	virtual int GetNumAxes();
	virtual float GetAxisDeadZone(int axis);
	virtual EJoyAxis GetAxisMap(int axis);
	virtual const char* GetAxisName(int axis);
	virtual float GetAxisScale(int axis);

	virtual void SetAxisDeadZone(int axis, float deadZone);
	virtual void SetAxisMap(int axis, EJoyAxis gameAxis);
	virtual void SetAxisScale(int axis, float scale);

	virtual bool IsSensitivityDefault();
	virtual bool IsAxisDeadZoneDefault(int axis);
	virtual bool IsAxisMapDefault(int axis);
	virtual bool IsAxisScaleDefault(int axis);

	virtual void SetDefaultConfig();
	virtual FString GetIdentifier();

	void AddAxes(float axes[NUM_JOYAXIS]) const;

	void Update();

	void UseAxesPolling(bool axesPolling);

	io_object_t* GetNotificationPtr();

private:
	IOHIDDeviceInterface** m_interface;
	IOHIDQueueInterface**  m_queue;

	FString m_name;
	FString m_identifier;

	float m_sensitivity;

	struct AnalogAxis
	{
		IOHIDElementCookie cookie;

		char name[64];

		float value;

		int32_t minValue;
		int32_t maxValue;

		float deadZone;
		float defaultDeadZone;
		float sensitivity;
		float defaultSensitivity;

		EJoyAxis gameAxis;
		EJoyAxis defaultGameAxis;

		AnalogAxis()
		{
			memset(this, 0, sizeof *this);
		}
	};

	TArray<AnalogAxis> m_axes;

	struct DigitalButton
	{
		IOHIDElementCookie cookie;
		int32_t            value;

		explicit DigitalButton(const IOHIDElementCookie cookie)
		: cookie(cookie)
		, value(0)
		{ }
	};

	TArray<DigitalButton> m_buttons;
	TArray<DigitalButton> m_POVs;

	bool m_useAxesPolling;

	io_object_t m_notification;


	static const float DEFAULT_DEADZONE;
	static const float DEFAULT_SENSITIVITY;

	void ProcessAxes();
	bool ProcessAxis  (const IOHIDEventStruct& event);
	bool ProcessButton(const IOHIDEventStruct& event);
	bool ProcessPOV   (const IOHIDEventStruct& event);

	void GatherDeviceInfo(io_object_t device, CFDictionaryRef properties);

	static void GatherElementsHandler(const void* value, void* parameter);
	void GatherCollectionElements(CFDictionaryRef properties);

	void AddAxis(CFDictionaryRef element);
	void AddButton(CFDictionaryRef element);
	void AddPOV(CFDictionaryRef element);

	void AddToQueue(IOHIDElementCookie cookie);
	void RemoveFromQueue(IOHIDElementCookie cookie);
};


const float IOKitJoystick::DEFAULT_DEADZONE    = 0.25f;
const float IOKitJoystick::DEFAULT_SENSITIVITY = 1.0f;


IOHIDDeviceInterface** CreateDeviceInterface(const io_object_t device)
{
	IOCFPlugInInterface** plugInInterface = NULL;
	SInt32 score = 0;

	const kern_return_t pluginResult = IOCreatePlugInInterfaceForService(device,
		kIOHIDDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plugInInterface, &score);

	IOHIDDeviceInterface** interface = NULL;

	if (KERN_SUCCESS == pluginResult)
	{
		// Call a method of the intermediate plug-in to create the device interface

		const HRESULT queryResult = (*plugInInterface)->QueryInterface(plugInInterface,
			CFUUIDGetUUIDBytes(kIOHIDDeviceInterfaceID), reinterpret_cast<LPVOID*>(&interface));

		IODestroyPlugInInterface(plugInInterface); // [?] or maybe (*plugInInterface)->Release(plugInInterface);

		if (S_OK == queryResult)
		{
			const IOReturn openResult = (*interface)->open(interface, 0);

			if (kIOReturnSuccess != openResult)
			{
				(*interface)->Release(interface);

				Printf(TEXTCOLOR_RED "IOHIDDeviceInterface::open() failed with code 0x%08X\n", openResult);
				return NULL;
			}
		}
		else
		{
			Printf(TEXTCOLOR_RED "IOCFPlugInInterface::QueryInterface() failed with code 0x%08X\n",
				static_cast<int>(queryResult));
			return NULL;
		}
	}
	else
	{
		Printf(TEXTCOLOR_RED "IOCreatePlugInInterfaceForService() failed with code %i\n", pluginResult);
		return NULL;
	}

	return interface;
}

IOHIDQueueInterface** CreateDeviceQueue(IOHIDDeviceInterface** const interface)
{
	if (NULL == interface)
	{
		return NULL;
	}

	IOHIDQueueInterface** queue = (*interface)->allocQueue(interface);

	if (NULL == queue)
	{
		Printf(TEXTCOLOR_RED "IOHIDDeviceInterface::allocQueue() failed\n");
		return NULL;
	}

	static const uint32_t QUEUE_FLAGS = 0;
	static const uint32_t QUEUE_DEPTH = 0;

	const IOReturn queueResult = (*queue)->create(queue, QUEUE_FLAGS, QUEUE_DEPTH);

	if (kIOReturnSuccess != queueResult)
	{
		(*queue)->Release(queue);

		Printf(TEXTCOLOR_RED "IOHIDQueueInterface::create() failed with code 0x%08X\n", queueResult);
		return NULL;
	}

	return queue;
}


IOKitJoystick::IOKitJoystick(const io_object_t device)
: m_interface(CreateDeviceInterface(device))
, m_queue(CreateDeviceQueue(m_interface))
, m_sensitivity(DEFAULT_SENSITIVITY)
, m_useAxesPolling(true)
, m_notification(0)
{
	if (NULL == m_interface || NULL == m_queue)
	{
		return;
	}

	CFMutableDictionaryRef properties = NULL;
	const kern_return_t propertiesResult =
		IORegistryEntryCreateCFProperties(device, &properties, kCFAllocatorDefault, kNilOptions);

	if (KERN_SUCCESS != propertiesResult || NULL == properties)
	{
		Printf(TEXTCOLOR_RED "IORegistryEntryCreateCFProperties() failed with code %i\n", propertiesResult);
		return;
	}

	GatherDeviceInfo(device, properties);
	GatherCollectionElements(properties);

	CFRelease(properties);

	UseAxesPolling(joy_axespolling);

	(*m_queue)->start(m_queue);

	SetDefaultConfig();
}

IOKitJoystick::~IOKitJoystick()
{
	M_SaveJoystickConfig(this);

	if (0 != m_notification)
	{
		IOObjectRelease(m_notification);
	}

	if (NULL != m_queue)
	{
		(*m_queue)->stop(m_queue);
		(*m_queue)->dispose(m_queue);
		(*m_queue)->Release(m_queue);
	}

	if (NULL != m_interface)
	{
		(*m_interface)->close(m_interface);
		(*m_interface)->Release(m_interface);
	}
}


FString IOKitJoystick::GetName()
{
	return m_name;
}


float IOKitJoystick::GetSensitivity()
{
	return m_sensitivity;
}

void IOKitJoystick::SetSensitivity(float scale)
{
	m_sensitivity = scale;
}


int IOKitJoystick::GetNumAxes()
{
	return static_cast<int>(m_axes.Size());
}

#define IS_AXIS_VALID (static_cast<unsigned int>(axis) < m_axes.Size())

float IOKitJoystick::GetAxisDeadZone(int axis)
{
	return IS_AXIS_VALID ? m_axes[axis].deadZone : 0.0f;
}

EJoyAxis IOKitJoystick::GetAxisMap(int axis)
{
	return IS_AXIS_VALID ? m_axes[axis].gameAxis : JOYAXIS_None;
}

const char* IOKitJoystick::GetAxisName(int axis)
{
	return IS_AXIS_VALID ? m_axes[axis].name : "Invalid";
}

float IOKitJoystick::GetAxisScale(int axis)
{
	return IS_AXIS_VALID ? m_axes[axis].sensitivity : 0.0f;
}

void IOKitJoystick::SetAxisDeadZone(int axis, float deadZone)
{
	if (IS_AXIS_VALID)
	{
		m_axes[axis].deadZone = clamp(deadZone, 0.0f, 1.0f);
	}
}

void IOKitJoystick::SetAxisMap(int axis, EJoyAxis gameAxis)
{
	if (IS_AXIS_VALID)
	{
		m_axes[axis].gameAxis = (gameAxis> JOYAXIS_None && gameAxis <NUM_JOYAXIS)
			? gameAxis
			: JOYAXIS_None;
	}
}

void IOKitJoystick::SetAxisScale(int axis, float scale)
{
	if (IS_AXIS_VALID)
	{
		m_axes[axis].sensitivity = scale;
	}
}


bool IOKitJoystick::IsSensitivityDefault()
{
	return DEFAULT_SENSITIVITY == m_sensitivity;
}

bool IOKitJoystick::IsAxisDeadZoneDefault(int axis)
{
	return IS_AXIS_VALID
		? (m_axes[axis].deadZone == m_axes[axis].defaultDeadZone)
		: true;
}

bool IOKitJoystick::IsAxisMapDefault(int axis)
{
	return IS_AXIS_VALID
		? (m_axes[axis].gameAxis == m_axes[axis].defaultGameAxis)
		: true;
}

bool IOKitJoystick::IsAxisScaleDefault(int axis)
{
	return IS_AXIS_VALID
		? (m_axes[axis].sensitivity == m_axes[axis].defaultSensitivity)
		: true;
}

#undef IS_AXIS_VALID

void IOKitJoystick::SetDefaultConfig()
{
	m_sensitivity = DEFAULT_SENSITIVITY;

	const size_t axisCount = m_axes.Size();

	for (size_t i = 0; i < axisCount; ++i)
	{
		m_axes[i].deadZone    = DEFAULT_DEADZONE;
		m_axes[i].sensitivity = DEFAULT_SENSITIVITY;
		m_axes[i].gameAxis    = JOYAXIS_None;
	}

	// Two axes? Horizontal is yaw and vertical is forward.

	if (2 == axisCount)
	{
		m_axes[0].gameAxis = JOYAXIS_Yaw;
		m_axes[1].gameAxis = JOYAXIS_Forward;
	}

	// Three axes? First two are movement, third is yaw.

	else if (axisCount >= 3)
	{
		m_axes[0].gameAxis = JOYAXIS_Side;
		m_axes[1].gameAxis = JOYAXIS_Forward;
		m_axes[2].gameAxis = JOYAXIS_Yaw;

		// Four axes? First two are movement, last two are looking around.

		if (axisCount >= 4)
		{
			m_axes[3].gameAxis = JOYAXIS_Pitch;
//	???		m_axes[3].sensitivity = 0.75f;

			// Five axes? Use the fifth one for moving up and down.

			if (axisCount >= 5)
			{
				m_axes[4].gameAxis = JOYAXIS_Up;
			}
		}
	}

	// If there is only one axis, then we make no assumptions about how
	// the user might want to use it.

	// Preserve defaults for config saving.

	for (size_t i = 0; i < axisCount; ++i)
	{
		m_axes[i].defaultDeadZone    = m_axes[i].deadZone;
		m_axes[i].defaultSensitivity = m_axes[i].sensitivity;
		m_axes[i].defaultGameAxis    = m_axes[i].gameAxis;
	}
}


FString IOKitJoystick::GetIdentifier()
{
	return m_identifier;
}


void IOKitJoystick::AddAxes(float axes[NUM_JOYAXIS]) const
{
	for (size_t i = 0, count = m_axes.Size(); i < count; ++i)
	{
		const EJoyAxis axis = m_axes[i].gameAxis;

		if (JOYAXIS_None == axis)
		{
			continue;
		}

		axes[axis] -= m_axes[i].value;
	}
}


void IOKitJoystick::UseAxesPolling(const bool axesPolling)
{
	m_useAxesPolling = axesPolling;

	for (size_t i = 0, count = m_axes.Size(); i < count; ++i)
	{
		AnalogAxis& axis = m_axes[i];

		if (m_useAxesPolling)
		{
			RemoveFromQueue(axis.cookie);
		}
		else
		{
			AddToQueue(axis.cookie);
		}
	}
}


void IOKitJoystick::Update()
{
	if (NULL == m_queue)
	{
		return;
	}

	IOHIDEventStruct event = { };
	AbsoluteTime  zeroTime = { };

	const IOReturn eventResult = (*m_queue)->getNextEvent(m_queue, &event, zeroTime, 0);

	if (kIOReturnSuccess == eventResult)
	{
		if (use_joystick)
		{
			ProcessAxis(event) || ProcessButton(event) || ProcessPOV(event);
		}
	}
	else if (kIOReturnUnderrun != eventResult)
	{
		Printf(TEXTCOLOR_RED "IOHIDQueueInterface::getNextEvent() failed with code 0x%08X\n", eventResult);
	}

	ProcessAxes();
}


void IOKitJoystick::ProcessAxes()
{
	if (NULL == m_interface || !m_useAxesPolling)
	{
		return;
	}

	for (size_t i = 0, count = m_axes.Size(); i < count; ++i)
	{
		AnalogAxis& axis = m_axes[i];

		static const double scaledMin = -1;
		static const double scaledMax =  1;

		IOHIDEventStruct event;

		if (kIOReturnSuccess == (*m_interface)->getElementValue(m_interface, axis.cookie, &event))
		{
			const double scaledValue = scaledMin +
				(event.value - axis.minValue) * (scaledMax - scaledMin) / (axis.maxValue - axis.minValue);
			const double filteredValue = Joy_RemoveDeadZone(scaledValue, axis.deadZone, NULL);

			axis.value = static_cast<float>(filteredValue * m_sensitivity * axis.sensitivity);
		}
		else
		{
			axis.value = 0.0f;
		}
	}
}


bool IOKitJoystick::ProcessAxis(const IOHIDEventStruct& event)
{
	if (m_useAxesPolling)
	{
		return false;
	}

	for (size_t i = 0, count = m_axes.Size(); i < count; ++i)
	{
		if (event.elementCookie != m_axes[i].cookie)
		{
			continue;
		}

		AnalogAxis& axis = m_axes[i];

		static const double scaledMin = -1;
		static const double scaledMax =  1;

		const double scaledValue = scaledMin +
			(event.value - axis.minValue) * (scaledMax - scaledMin) / (axis.maxValue - axis.minValue);
		const double filteredValue = Joy_RemoveDeadZone(scaledValue, axis.deadZone, NULL);

		axis.value = static_cast<float>(filteredValue * m_sensitivity * axis.sensitivity);

		return true;
	}

	return false;
}

bool IOKitJoystick::ProcessButton(const IOHIDEventStruct& event)
{
	for (size_t i = 0, count = m_buttons.Size(); i < count; ++i)
	{
		if (event.elementCookie != m_buttons[i].cookie)
		{
			continue;
		}

		int32_t& current = m_buttons[i].value;
		const int32_t previous = current;
		current = event.value;

		Joy_GenerateButtonEvents(previous, current, 1, static_cast<int>(KEY_FIRSTJOYBUTTON + i));

		return true;
	}

	return false;
}

bool IOKitJoystick::ProcessPOV(const IOHIDEventStruct& event)
{
	for (size_t i = 0, count = m_POVs.Size(); i <count; ++i)
	{
		if (event.elementCookie != m_POVs[i].cookie)
		{
			continue;
		}

		int32_t& current = m_buttons[i].value;
		const int32_t previous = current;
		current = event.value;

		static const int POV_BUTTONS[] =
		{
			0x01, 0x03, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x09, 0x00
		};

		Joy_GenerateButtonEvents(POV_BUTTONS[previous], POV_BUTTONS[current], 4, KEY_JOYPOV1_UP + i * 4);

		return true;
	}

	return false;
}


void IOKitJoystick::GatherDeviceInfo(const io_object_t device, const CFDictionaryRef properties)
{
	assert(NULL != properties);

	CFStringRef vendorRef = static_cast<CFStringRef>(
		CFDictionaryGetValue(properties, CFSTR(kIOHIDManufacturerKey)));
	CFStringRef productRef = static_cast<CFStringRef>(
		CFDictionaryGetValue(properties, CFSTR(kIOHIDProductKey)));
	CFNumberRef vendorIDRef = static_cast<CFNumberRef>(
		CFDictionaryGetValue(properties, CFSTR(kIOHIDVendorIDKey)));
	CFNumberRef productIDRef = static_cast<CFNumberRef>(
		CFDictionaryGetValue(properties, CFSTR(kIOHIDProductIDKey)));

	CFMutableDictionaryRef usbProperties = NULL;

	if (   NULL ==   vendorRef || NULL ==   productRef
		|| NULL == vendorIDRef || NULL == productIDRef)
	{
		// OS X is not mirroring all USB properties to HID page, so need to look at USB device page also
		// Step up two levels and get dictionary of USB properties

		io_registry_entry_t parent1;
		kern_return_t ioResult = IORegistryEntryGetParentEntry(device,  kIOServicePlane, &parent1);

		if (KERN_SUCCESS == ioResult)
		{
			io_registry_entry_t parent2;
			ioResult = IORegistryEntryGetParentEntry(device,  kIOServicePlane, &parent2);

			if (KERN_SUCCESS == ioResult)
			{
				ioResult = IORegistryEntryCreateCFProperties(parent2, &usbProperties, kCFAllocatorDefault, kNilOptions);

				if (KERN_SUCCESS != ioResult)
				{
					Printf(TEXTCOLOR_RED "IORegistryEntryCreateCFProperties() failed with code %i\n", ioResult);
				}

				IOObjectRelease(parent2);
			}
			else
			{
				Printf(TEXTCOLOR_RED "IORegistryEntryGetParentEntry(2) failed with code %i\n", ioResult);
			}

			IOObjectRelease(parent1);
		}
		else
		{
			Printf(TEXTCOLOR_RED "IORegistryEntryGetParentEntry(1) failed with code %i\n", ioResult);
		}
	}

	if (NULL != usbProperties)
	{
		if (NULL == vendorRef)
		{
			vendorRef = static_cast<CFStringRef>(
				CFDictionaryGetValue(usbProperties, CFSTR("USB Vendor Name")));
		}

		if (NULL == productRef)
		{
			productRef = static_cast<CFStringRef>(
				CFDictionaryGetValue(usbProperties, CFSTR("USB Product Name")));
		}

		if (NULL == vendorIDRef)
		{
			vendorIDRef = static_cast<CFNumberRef>(
				CFDictionaryGetValue(usbProperties, CFSTR("idVendor")));
		}

		if (NULL == productIDRef)
		{
			productIDRef = static_cast<CFNumberRef>(
				CFDictionaryGetValue(usbProperties, CFSTR("idProduct")));
		}
	}

	m_name += ToFString(vendorRef);
	m_name += " ";
	m_name += ToFString(productRef);

	int vendorID = 0, productID = 0;

	if (NULL != vendorIDRef)
	{
		CFNumberGetValue(vendorIDRef, kCFNumberIntType, &vendorID);
	}

	if (NULL != productIDRef)
	{
		CFNumberGetValue(productIDRef, kCFNumberIntType, &productID);
	}

	m_identifier.AppendFormat("VID_%04x_PID_%04x", vendorID, productID);

	if (NULL != usbProperties)
	{
		CFRelease(usbProperties);
	}
}


long GetElementValue(const CFDictionaryRef element, const CFStringRef key)
{
	const CFNumberRef number =
		static_cast<CFNumberRef>(CFDictionaryGetValue(element, key));
	long result = 0;

	if (NULL != number && CFGetTypeID(number) == CFNumberGetTypeID())
	{
		CFNumberGetValue(number, kCFNumberLongType, &result);
	}

	return result;
}

void IOKitJoystick::GatherElementsHandler(const void* value, void* parameter)
{
	assert(NULL != value);
	assert(NULL != parameter);

	const CFDictionaryRef element = static_cast<CFDictionaryRef>(value);
	IOKitJoystick* thisPtr = static_cast<IOKitJoystick*>(parameter);

	if (CFGetTypeID(element) != CFDictionaryGetTypeID())
	{
		Printf(TEXTCOLOR_RED "IOKitJoystick: Encountered wrong element type\n");
		return;
	}

	const long type = GetElementValue(element, CFSTR(kIOHIDElementTypeKey));

	if (kIOHIDElementTypeCollection == type)
	{
		thisPtr->GatherCollectionElements(element);
	}
	else if (0 != type)
	{
		const long usagePage = GetElementValue(element, CFSTR(kIOHIDElementUsagePageKey));

		if (kHIDPage_GenericDesktop == usagePage)
		{
			const long usage = GetElementValue(element, CFSTR(kIOHIDElementUsageKey));

			if (   kHIDUsage_GD_Slider == usage
				|| kHIDUsage_GD_X      == usage || kHIDUsage_GD_Y  == usage || kHIDUsage_GD_Z  == usage
				|| kHIDUsage_GD_Rx     == usage || kHIDUsage_GD_Ry == usage || kHIDUsage_GD_Rz == usage)
			{
				thisPtr->AddAxis(element);
			}
			else if (kHIDUsage_GD_Hatswitch == usage && thisPtr->m_POVs.Size() < 4)
			{
				thisPtr->AddPOV(element);
			}
		}
		else if (kHIDPage_Button == usagePage)
		{
			thisPtr->AddButton(element);
		}
	}
}

void IOKitJoystick::GatherCollectionElements(const CFDictionaryRef properties)
{
	const CFArrayRef topElement = static_cast<CFArrayRef>(
		CFDictionaryGetValue(properties, CFSTR(kIOHIDElementKey)));

	if (NULL == topElement || CFGetTypeID(topElement) != CFArrayGetTypeID())
	{
		Printf(TEXTCOLOR_RED "GatherCollectionElements: invalid properties dictionary\n");
		return;
	}

	const CFRange range = { 0, CFArrayGetCount(topElement) };

	CFArrayApplyFunction(topElement, range, GatherElementsHandler, this);
}


IOHIDElementCookie GetElementCookie(const CFDictionaryRef element)
{
	// Use C-style cast to avoid 32/64-bit IOHIDElementCookie type issue
	return (IOHIDElementCookie)GetElementValue(element, CFSTR(kIOHIDElementCookieKey));
}

void IOKitJoystick::AddAxis(const CFDictionaryRef element)
{
	AnalogAxis axis;

	axis.cookie   = GetElementCookie(element);
	axis.minValue = GetElementValue(element, CFSTR(kIOHIDElementMinKey));
	axis.maxValue = GetElementValue(element, CFSTR(kIOHIDElementMaxKey));

	const CFStringRef nameRef = static_cast<CFStringRef>(
		CFDictionaryGetValue(element, CFSTR(kIOHIDElementNameKey)));

	if (NULL != nameRef && CFStringGetTypeID() == CFGetTypeID(nameRef))
	{
		CFStringGetCString(nameRef, axis.name, sizeof(axis.name) - 1, kCFStringEncodingUTF8);
	}
	else
	{
		snprintf(axis.name, sizeof(axis.name), "Axis %i", m_axes.Size() + 1);
	}

	m_axes.Push(axis);
}

void IOKitJoystick::AddButton(CFDictionaryRef element)
{
	const DigitalButton button(GetElementCookie(element));

	m_buttons.Push(button);

	AddToQueue(button.cookie);
}

void IOKitJoystick::AddPOV(CFDictionaryRef element)
{
	const DigitalButton pov(GetElementCookie(element));

	m_POVs.Push(pov);

	AddToQueue(pov.cookie);
}


void IOKitJoystick::AddToQueue(const IOHIDElementCookie cookie)
{
	if (NULL == m_queue)
	{
		return;
	}

	if (!(*m_queue)->hasElement(m_queue, cookie))
	{
		(*m_queue)->addElement(m_queue, cookie, 0);
	}
}

void IOKitJoystick::RemoveFromQueue(const IOHIDElementCookie cookie)
{
	if (NULL == m_queue)
	{
		return;
	}

	if ((*m_queue)->hasElement(m_queue, cookie))
	{
		(*m_queue)->removeElement(m_queue, cookie);
	}
}


io_object_t* IOKitJoystick::GetNotificationPtr()
{
	return &m_notification;
}


// ---------------------------------------------------------------------------


class IOKitJoystickManager
{
public:
	IOKitJoystickManager();
	~IOKitJoystickManager();

	void GetJoysticks(TArray<IJoystickConfig*>& joysticks) const;

	void AddAxes(float axes[NUM_JOYAXIS]) const;

	// Updates axes/buttons states
	void Update();

	void UseAxesPolling(bool axesPolling);

private:
	typedef TDeletingArray<IOKitJoystick*> JoystickList;
	JoystickList m_joysticks;

	static const size_t NOTIFICATION_PORT_COUNT = 2;

	IONotificationPortRef m_notificationPorts[NOTIFICATION_PORT_COUNT];
	io_iterator_t         m_notifications    [NOTIFICATION_PORT_COUNT];

	// Rebuilds device list
	void Rescan(int usagePage, int usage, size_t notificationPortIndex);
	void AddDevices(IONotificationPortRef notificationPort, const io_iterator_t iterator);

	static void OnDeviceAttached(void* refcon, io_iterator_t iterator);
	static void OnDeviceRemoved(void* refcon, io_service_t service,
		natural_t messageType, void* messageArgument);
};


IOKitJoystickManager* s_joystickManager;


IOKitJoystickManager::IOKitJoystickManager()
{
	memset(m_notifications, 0, sizeof m_notifications);

	for (size_t i = 0; i < NOTIFICATION_PORT_COUNT; ++i)
	{
		m_notificationPorts[i] = IONotificationPortCreate(kIOMasterPortDefault);

		if (NULL == m_notificationPorts[i])
		{
			Printf(TEXTCOLOR_RED "IONotificationPortCreate(%zu) failed\n", i);
			return;
		}

		CFRunLoopAddSource(CFRunLoopGetCurrent(),
			IONotificationPortGetRunLoopSource(m_notificationPorts[i]), kCFRunLoopDefaultMode);
	}

	Rescan(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick, 0);
	Rescan(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad,  1);
}

IOKitJoystickManager::~IOKitJoystickManager()
{
	for (size_t i = 0; i < NOTIFICATION_PORT_COUNT; ++i)
	{
		IONotificationPortRef& port = m_notificationPorts[i];

		if (NULL != port)
		{
			CFRunLoopRemoveSource(CFRunLoopGetCurrent(),
				IONotificationPortGetRunLoopSource(port), kCFRunLoopDefaultMode);

			IONotificationPortDestroy(port);
			port = NULL;
		}

		io_iterator_t& notification = m_notifications[i];

		if (0 != notification)
		{
			IOObjectRelease(notification);
			notification = 0;
		}
	}
}


void IOKitJoystickManager::GetJoysticks(TArray<IJoystickConfig*>& joysticks) const
{
	const size_t joystickCount = m_joysticks.Size();

	joysticks.Resize(joystickCount);

	for (size_t i = 0; i < joystickCount; ++i)
	{
		M_LoadJoystickConfig(m_joysticks[i]);

		joysticks[i] = m_joysticks[i];
	}
}

void IOKitJoystickManager::AddAxes(float axes[NUM_JOYAXIS]) const
{
	for (size_t i = 0, count = m_joysticks.Size(); i < count; ++i)
	{
		m_joysticks[i]->AddAxes(axes);
	}
}


void IOKitJoystickManager::Update()
{
	for (size_t i = 0, count = m_joysticks.Size(); i < count; ++i)
	{
		m_joysticks[i]->Update();
	}
}


void IOKitJoystickManager::UseAxesPolling(const bool axesPolling)
{
	for (size_t i = 0, count = m_joysticks.Size(); i < count; ++i)
	{
		m_joysticks[i]->UseAxesPolling(axesPolling);
	}
}


void PostDeviceChangeEvent()
{
	event_t event = { EV_DeviceChange };
	D_PostEvent(&event);
}


void IOKitJoystickManager::Rescan(const int usagePage, const int usage, const size_t notificationPortIndex)
{
	CFMutableDictionaryRef deviceMatching = IOServiceMatching(kIOHIDDeviceKey);

	if (NULL == deviceMatching)
	{
		Printf(TEXTCOLOR_RED "IOServiceMatching() returned NULL\n");
		return;
	}

	const CFNumberRef usagePageRef =
		CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usagePage);
	CFDictionarySetValue(deviceMatching, CFSTR(kIOHIDPrimaryUsagePageKey), usagePageRef);

	const CFNumberRef usageRef =
		CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage);
	CFDictionarySetValue(deviceMatching, CFSTR(kIOHIDPrimaryUsageKey), usageRef);

	assert(notificationPortIndex < NOTIFICATION_PORT_COUNT);
	io_iterator_t* iteratorPtr = &m_notifications[notificationPortIndex];

	const IONotificationPortRef notificationPort = m_notificationPorts[notificationPortIndex];
	assert(NULL != notificationPort);

	const kern_return_t notificationResult = IOServiceAddMatchingNotification(notificationPort,
		kIOFirstMatchNotification, deviceMatching, OnDeviceAttached, notificationPort, iteratorPtr);

	// IOServiceAddMatchingNotification() consumes one reference of matching dictionary
	// Thus CFRelease(deviceMatching) is not needed

	CFRelease(usageRef);
	CFRelease(usagePageRef);

	if (KERN_SUCCESS != notificationResult)
	{
		Printf(TEXTCOLOR_RED "IOServiceAddMatchingNotification() failed with code %i\n", notificationResult);
	}

	AddDevices(notificationPort, *iteratorPtr);
}

void IOKitJoystickManager::AddDevices(const IONotificationPortRef notificationPort, const io_iterator_t iterator)
{
	while (io_object_t device = IOIteratorNext(iterator))
	{
		IOKitJoystick* joystick = new IOKitJoystick(device);
		m_joysticks.Push(joystick);

		const kern_return_t notificationResult = IOServiceAddInterestNotification(notificationPort,
			device, kIOGeneralInterest, OnDeviceRemoved, joystick, joystick->GetNotificationPtr());
		if (KERN_SUCCESS != notificationResult)
		{
			Printf(TEXTCOLOR_RED "IOServiceAddInterestNotification() failed with code %i\n", notificationResult);
		}

		IOObjectRelease(device);
		
		PostDeviceChangeEvent();
	}
}


void IOKitJoystickManager::OnDeviceAttached(void* const refcon, const io_iterator_t iterator)
{
	assert(NULL != refcon);
	const IONotificationPortRef notificationPort = static_cast<IONotificationPortRef>(refcon);

	assert(NULL != s_joystickManager);
	s_joystickManager->AddDevices(notificationPort, iterator);
}

void IOKitJoystickManager::OnDeviceRemoved(void* const refcon, io_service_t, const natural_t messageType, void*)
{
	if (messageType != kIOMessageServiceIsTerminated)
	{
		return;
	}

	assert(NULL != refcon);
	IOKitJoystick* const joystick = static_cast<IOKitJoystick*>(refcon);

	assert(NULL != s_joystickManager);
	JoystickList& joysticks = s_joystickManager->m_joysticks;

	for (unsigned int i = 0, count = joysticks.Size(); i < count; ++i)
	{
		if (joystick == joysticks[i])
		{
			joysticks.Delete(i);
			break;
		}
	}

	delete joystick;

	PostDeviceChangeEvent();
}

} // unnamed namespace


// ---------------------------------------------------------------------------


void I_ShutdownInput()
{
	delete s_joystickManager;
	s_joystickManager = NULL;
}

void I_GetJoysticks(TArray<IJoystickConfig*>& sticks)
{
	// Instances of IOKitJoystick depend on GameConfig object.
	// M_SaveDefaultsFinal() must be called after destruction of IOKitJoystickManager.
	// To ensure this, its initialization is moved here.
	// As M_LoadDefaults() was already called at this moment,
	// the order of atterm's functions will be correct

	if (NULL == s_joystickManager && !Args->CheckParm("-nojoy"))
	{
		s_joystickManager = new IOKitJoystickManager;
	}

	if (NULL != s_joystickManager)
	{
		s_joystickManager->GetJoysticks(sticks);
	}
}

void I_GetAxes(float axes[NUM_JOYAXIS])
{
	for (size_t i = 0; i < NUM_JOYAXIS; ++i)
	{
		axes[i] = 0.0f;
	}

	if (use_joystick && NULL != s_joystickManager)
	{
		s_joystickManager->AddAxes(axes);
	}
}

IJoystickConfig* I_UpdateDeviceList()
{
	// Does nothing, device list is always kept up-to-date

	return NULL;
}


// ---------------------------------------------------------------------------


void I_ProcessJoysticks()
{
	if (NULL != s_joystickManager)
	{
		s_joystickManager->Update();
	}
}


// ---------------------------------------------------------------------------


CUSTOM_CVAR(Bool, joy_axespolling, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_NOINITCALL)
{
	if (NULL != s_joystickManager)
	{
		s_joystickManager->UseAxesPolling(self);
	}
}