/*
 *  Copyright (C) 2010-2024 Fabio Cavallo (aka FHorse)
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include "gui.h"
#include "conf.h"

#define UHID_DEV_PATH "/dev/uhid"
#define UJOY_DEV_PATH "/dev/ujoy/"
#define USB_DEV_PATH "/dev/usb"

enum _js_bsd_hat {
	JS_OS_HAT_UP,
	JS_OS_HAT_UP_RIGHT,
	JS_OS_HAT_RIGHT,
	JS_OS_HAT_RIGHT_DOWN,
	JS_OS_HAT_DOWN,
	JS_OS_HAT_DOWN_LEFT,
	JS_OS_HAT_LEFT,
	JS_OS_HAT_LEFT_UP,
};

typedef struct _js_usb_hid_device {
	uTCHAR desc[128];
	WORD bustype;
	WORD vendor_id;
	WORD product_id;
	WORD version;
	unsigned int flags;
} _js_usb_hid_device;

INLINE static BYTE is_abs_y_xinput(_js_device *jdev, _js_axis *jsx);
INLINE static void usb_dev_scan(void);
INLINE static void usb_dev_info(_js_usb_hid_device *uhdev, struct usb_device_info *udi);
INLINE static void hat_validate(_js_device *jdev, int index, SDBWORD value);
INLINE static void hat_to_xy(SDBWORD hat, SDBWORD *x, SDBWORD *y);

struct _js_os {
	struct _usb_devices_detected {
		int count;
		_js_usb_hid_device devices[MAX_JOYSTICK];
	} udd;
} js_os;

void js_os_init(UNUSED(BYTE first_time)) {}
void js_os_quit(UNUSED(BYTE last_time)) {}
void js_os_jdev_init(_js_device *jdev) {
	jdev->fd = -1;
	umemset(jdev->dev, 0x00, usizeof(jdev->dev));

	jdev->hug_d_pad_state = 0;

	jdev->repdesc = NULL;
	jdev->report.buf = NULL;
	jdev->report.id = -1;
	jdev->report.size = 0;
}
void js_os_jdev_open(_js_device *jdev, UNUSED(void *arg)) {
	struct hid_item hitem;
	struct hid_data *hdata;
	BYTE is_a_joystick = FALSE;

	js_jdev_init(jdev);

#if defined (__OpenBSD__)
	usnprintf(jdev->dev, usizeof(jdev->dev), uL("" UJOY_DEV_PATH "%d"), jdev->index);
#else
	usnprintf(jdev->dev, usizeof(jdev->dev), uL("" UHID_DEV_PATH "%d"), jdev->index);
#endif
	if ((jdev->fd = uopen(jdev->dev, O_RDONLY | O_NONBLOCK)) < 0) {
		return;
	}

	{
		_js_usb_hid_device *uhdev = NULL;
#if defined (__FreeBSD__)
		struct usb_device_info udi;
		_js_usb_hid_device uhd;

		if (ioctl(jdev->fd, USB_GET_DEVICEINFO, &udi) != -1) {
			usb_dev_info(&uhd, &udi);
			uhdev = &uhd;
		} else {
			ustrncpy(jdev->desc, uL("Unknow"), usizeof(jdev->desc) - 1);
		}
#else
		uhdev = &js_os.udd.devices[jdev->index];
#endif
		if (uhdev) {
			umemcpy(&jdev->desc, uhdev->desc, usizeof(jdev->desc) - 1);
			jdev->usb.bustype = uhdev->bustype;
			jdev->usb.vendor_id = uhdev->vendor_id;
			jdev->usb.product_id = uhdev->product_id;
			jdev->usb.version = uhdev->version;
		} else {
			ustrncpy(jdev->desc, uL("Unknow"), usizeof(jdev->desc) - 1);
		}
	}

	if ((jdev->repdesc = hid_get_report_desc(jdev->fd)) == NULL) {
		log_error(uL("" uPs("") ";hid_get_report_desc, %s"), jdev->dev, strerror(errno));
		js_os_jdev_close(jdev);
		return;
	}

	js_jdev_type(jdev);
	jdev->is_xinput = js_jdev_is_xinput(jdev);

#if defined (__FreeBSD__)
	if ((jdev->report.id = hid_get_report_id(jdev->fd)) < 0) {
		jdev->report.id = -1;
	}
#else
	if (ioctl(jdev->fd, USB_GET_REPORT_ID, &jdev->report.id) < 0) {
		jdev->report.id = -1;
	}
#endif

#if defined (__DragonFly__)
	jdev->report.size = hid_report_size(jdev->repdesc, jdev->report.id, hid_input);
#elif defined (__FreeBSD__)
	jdev->report.size = hid_report_size(jdev->repdesc, hid_input, jdev->report.id);
#elif defined (USBHID_NEW)
	jdev->report.size = hid_report_size(jdev->repdesc, hid_input, jdev->report.id);
#else
	jdev->report.size = hid_report_size(jdev->repdesc, hid_input, &jdev->report.id);
#endif
	if (jdev->report.size <= 0) {
		log_error(uL("" uPs("") ";hid_report_size() return invalid size"), jdev->dev);
		js_os_jdev_close(jdev);
		return;
	}

#if defined (__FreeBSD__)
	jdev->report.buf = malloc(jdev->report.size);
#else
	jdev->report.buf = malloc(sizeof(*jdev->report.buf) - sizeof(REP_BUF_DATA(jdev->report)) + jdev->report.size);
#endif
	if (jdev->report.buf == NULL) {
		log_error(uL("" uPs("") ";error on malloc report buf"), jdev->dev);
		js_os_jdev_close(jdev);
		return;
	}

#if defined (USBHID_NEW) || defined (__FreeBSD__) || defined (__FreeBSD_kernel__)
	hdata = hid_start_parse(jdev->repdesc, 1 << hid_input, jdev->report.id);
#else
	hdata = hid_start_parse(jdev->repdesc, 1 << hid_input);
#endif
	if (hdata == NULL) {
		log_error(uL("" uPs("") ";hid_start_parse() don't start"), jdev->dev);
		js_os_jdev_close(jdev);
		return;
	}

	while (hid_get_item(hdata, &hitem) > 0) {
		unsigned int usage = HID_USAGE(hitem.usage);
		unsigned int page = HID_PAGE(hitem.usage);

		switch (hitem.kind) {
			case hid_collection:
				if ((page == HUP_GENERIC_DESKTOP) && ((usage == HUG_JOYSTICK) || (usage == HUG_GAME_PAD))) {
					is_a_joystick = TRUE;
				}
				break;
			case hid_input:
				if (page == HUP_GENERIC_DESKTOP) {
					_js_axis *jsx;

					switch (usage) {
						case ABS_X:
						case ABS_Y:
						case ABS_Z:
						case ABS_RX:
						case ABS_RY:
						case ABS_RZ:
						case ABS_THROTTLE:
						case ABS_RUDDER:
						case ABS_WHEEL:
							jsx = &jdev->data.axis[jdev->info.axes];
							jsx->used = TRUE;
							jsx->offset = usage;
							jsx->min = is_abs_y_xinput(jdev, jsx) ? -hitem.logical_maximum : hitem.logical_minimum;
							jsx->max = is_abs_y_xinput(jdev, jsx) ? -hitem.logical_minimum : hitem.logical_maximum;
							jsx->center = 0;
							jsx->scale = (float)(JS_AXIS_MAX - JS_AXIS_MIN) / (float)(jsx->max - jsx->min);
							jdev->info.axes++;
							continue;
						case HUG_D_PAD_UP:
						case HUG_D_PAD_LEFT:
						case HUG_D_PAD_RIGHT:
							continue;
						case HUG_D_PAD_DOWN:
							usage = HUG_HAT_SWITCH;
							break;
						default:
							break;
					}
					if (usage == HUG_HAT_SWITCH) {
						if (jdev->info.hats < JS_MAX_HATS) {
							int a, index = jdev->info.hats * 2;
 							_js_axis *jsx;

							for (a = 0; a < 2; a++) {
								jsx = &jdev->data.hat[index + a];
								jsx->used = TRUE;
								jsx->offset = HUG_HAT_SWITCH + a | 0x800;
								jsx->min = JS_AXIS_MIN;
								jsx->max = JS_AXIS_MAX;
								jsx->center = 0;
								jsx->scale = (float)(JS_AXIS_MAX - JS_AXIS_MIN) / (float)(jsx->max - jsx->min);
								jsx->is_hat = TRUE;
							}
							jdev->info.hats++;
						}
					}
				} else if (page == HUP_BUTTON) {
					if (jdev->info.buttons < JS_MAX_BUTTONS) {
						_js_button *jsx = &jdev->data.button[jdev->info.buttons];

						jsx->used = TRUE;
						jsx->offset = usage;
						jdev->info.buttons++;
					}
				}
				break;
			default:
				break;
		}
	}
	hid_end_parse(hdata);

	if (!is_a_joystick && ((jdev->info.axes == 0) && (jdev->info.buttons == 0) && (jdev->info.hats == 0))) {
		log_error(uL("" uPs("") ";is not a joystick"), jdev->dev);
		js_os_jdev_close(jdev);
		return;
	}

	js_guid_create(jdev);
	jdev->present = TRUE;
	jstick.jdd.count++;

	js_jdev_open_common(jdev);

#if defined (DEBUG)
	js_info_jdev(jdev);
#endif
}
void js_os_jdev_close(_js_device *jdev) {
	if (jdev == NULL) {
		return;
	}

	if (jdev->present) {
		jstick.jdd.count--;
#if defined (DEBUG)
		log_warning(uL("jstick disc.;slot%d \"" uPs("") "\" (%d)"), jdev->index, jdev->desc, jstick.jdd.count);
#endif
	}

	jdev->present = FALSE;
	jdev->is_xinput = FALSE;
	js_guid_unset(&jdev->guid);
	umemset(jdev->dev, 0x00, usizeof(jdev->dev));

	if (jdev->fd >= 0) {
		close(jdev->fd);
		jdev->fd = -1;
	}
	if (jdev->repdesc) {
		hid_dispose_report_desc(jdev->repdesc);
		jdev->repdesc = NULL;
	}
	if (jdev->report.buf) {
		free(jdev->report.buf);
		jdev->report.buf = NULL;
	}
}
void js_os_jdev_scan(void) {
	int i;

	usb_dev_scan();

	for (i = 0; i < MAX_JOYSTICK; i++) {
		_js_device *jdev = &jstick.jdd.devices[i];

		thread_mutex_lock(jdev->lock);

		if (!jdev->present) {
			js_os_jdev_open(jdev, NULL);
		}

		thread_mutex_unlock(jdev->lock);
	}
}
void js_os_jdev_read_events_loop(_js_device *jdev) {
	int i;

	while (read(jdev->fd, REP_BUF_DATA(jdev->report), jdev->report.size) == jdev->report.size) {
		struct hid_item hitem;
		struct hid_data *hdata;
		int hat = 0;

#if defined (USBHID_NEW) || defined (__FreeBSD__) || defined (__FreeBSD_kernel__)
		hdata = hid_start_parse(jdev->repdesc, 1 << hid_input, jdev->report.id);
#else
		hdata = hid_start_parse(jdev->repdesc, 1 << hid_input);
#endif
		if (hdata == NULL) {
			continue;
		}

		while (hid_get_item(hdata, &hitem) > 0) {
			unsigned int usage = HID_USAGE(hitem.usage);
			unsigned int page = HID_PAGE(hitem.usage);
			BYTE read_hat_value = TRUE;
			SDBWORD value;

			if (hitem.kind != hid_input) {
				continue;
			}

			if (page == HUP_GENERIC_DESKTOP) {
				if ((usage >= HUG_D_PAD_UP) && (usage <= HUG_D_PAD_LEFT)) {
					value = !!hid_get_data(REP_BUF_DATA(jdev->report), &hitem);

					if (usage == HUG_D_PAD_UP) {
						jdev->hug_d_pad_state = (jdev->hug_d_pad_state & 0x0E) | (value << 0);
					} else if (usage == HUG_D_PAD_DOWN) {
						jdev->hug_d_pad_state = (jdev->hug_d_pad_state & 0x0D) | (value << 1);
					} else if (usage == HUG_D_PAD_LEFT) {
						jdev->hug_d_pad_state = (jdev->hug_d_pad_state & 0x0B) | (value << 2);
					} else if (usage == HUG_D_PAD_RIGHT) {
						jdev->hug_d_pad_state = (jdev->hug_d_pad_state & 0x07) | (value << 3);
					}

					switch (jdev->hug_d_pad_state) {
						default:
						case 0:
							value = 0x0F;
							break;
						case 1:
							value = JS_OS_HAT_UP;
							break;
						case 9:
							value = JS_OS_HAT_UP_RIGHT;
							break;
						case 8:
							value = JS_OS_HAT_RIGHT;
							break;
						case 10:
							value = JS_OS_HAT_RIGHT_DOWN;
							break;
						case 2:
							value = JS_OS_HAT_DOWN;
							break;
						case 6:
							value = JS_OS_HAT_DOWN_LEFT;
							break;
						case 4:
							value = JS_OS_HAT_LEFT;
							break;
						case 5:
							value = JS_OS_HAT_LEFT_UP;
							break;
					}

					if (usage == HUG_D_PAD_DOWN) {
						read_hat_value = FALSE;
						usage = HUG_HAT_SWITCH;
					} else {
						continue;
					}
				}

				switch (usage) {
					case ABS_X:
					case ABS_Y:
					case ABS_Z:
					case ABS_RX:
					case ABS_RY:
					case ABS_RZ:
					case ABS_THROTTLE:
					case ABS_RUDDER:
					case ABS_WHEEL:
						for (i = 0; i < JS_MAX_AXES; i++) {
							_js_axis *jsx = &jdev->data.axis[i];

							if ((jsx->offset == usage) && jsx->used) {
								value = hid_get_data(REP_BUF_DATA(jdev->report), &hitem);
								js_axs_validate(jsx, is_abs_y_xinput(jdev, jsx) ? -value : value);
							}
						}
						continue;
					case HUG_HAT_SWITCH:
						if (hat < jdev->info.hats) {
							if (read_hat_value) {
								value = hid_get_data(REP_BUF_DATA(jdev->report), &hitem);
							}
							hat_validate(jdev, hat * 2, value);
							hat++;
						}
						continue;
					default:
						continue;
				}
			} else if (page == HUP_BUTTON) {
				for (i = 0; i < JS_MAX_BUTTONS; i++) {
					_js_button *jsx = &jdev->data.button[i];

					if ((jsx->offset == usage) && jsx->used) {
						value = hid_get_data(REP_BUF_DATA(jdev->report), &hitem);
						jsx->value = !!value;
					}
				}
			}
		}
		hid_end_parse(hdata);
	}

	if (errno != EAGAIN) {
		js_os_jdev_close(jdev);
		return;
	}
}

INLINE static BYTE is_abs_y_xinput(_js_device *jdev, _js_axis *jsx) {
	return (((jsx->offset == ABS_Y) || (jsx->offset == ABS_RY)) && jdev->is_xinput);
}
INLINE static void usb_dev_scan(void) {
#if !defined (__FreeBSD__)
	int i;

	js_os.udd.count = 0;

	for (i = 0; i < MAX_JOYSTICK; i++) {
		uTCHAR dev[30];
		int a, fd;

		usnprintf(dev, usizeof(dev), uL("" USB_DEV_PATH "%d"), i);

		if ((fd = uopen(dev, O_RDONLY | O_NONBLOCK)) < 0) {
			continue;
		}

		for (a = 1; a < USB_MAX_DEVICES; a++) {
			struct usb_device_info udi;
			int b;

			udi.udi_addr = a;

			if (ioctl(fd, USB_DEVICEINFO, &udi) == -1) {
				continue;
			}

			for (b = 0; b < USB_MAX_DEVNAMES; b++) {
				_js_usb_hid_device *uhidev = &js_os.udd.devices[js_os.udd.count];

				if (ustrncmp(udi.udi_devnames[b], uL("uhidev"), 6) != 0) {
					continue;
				}
				usb_dev_info(uhidev, &udi);
				js_os.udd.count++;
			}
		}
		close(fd);
	}
#endif
}
INLINE static void usb_dev_info(_js_usb_hid_device *uhdev, struct usb_device_info *udi) {
	BYTE vendor = FALSE;
	unsigned int size;

	umemset(uhdev->desc, 0x00, usizeof(uhdev->desc));

#if defined (__NetBSD__)
	{
		usb_device_descriptor_t uddesc;
		struct usb_string_desc usd;

		if (ioctl(fd, USB_GET_DEVICE_DESC, &uddesc) != -1) {
			// Get default language
			usd.usd_string_index = USB_LANGUAGE_TABLE;
			usd.usd_language_id = 0;

			if ((ioctl(fd, USB_GET_STRING_DESC, &usd) == -1) || (usd.usd_desc.bLength < 4)) {
				usd.usd_language_id = 0;
			} else {
				usd.usd_language_id = UGETW(usd.usd_desc.bString[0]);
			}

			usd.usd_string_index = uddesc.iProduct;

			if (ioctl(fd, USB_GET_STRING_DESC, &usd) == 0) {
				int i;

				for (i = 0; i < (usd.usd_desc.bLength >> 1) - 1 && i < sizeof(str) - 1; i++) {
					uhdev->desc[i] = UGETW(usd.usd_desc.bString[i]);
				}
				uhdev->desc[i] = '\0';
			}
		}
	}
#else
	size = ustrlen(udi->udi_product) + 1;

	if (ustrlen(udi->udi_vendor) && ustrncmp(udi->udi_vendor, uL("vendor 0x"), 9) != 0) {
		size += ustrlen(udi->udi_vendor) + 1;
		vendor = TRUE;
	}

	size = FHMIN(size, (int)(usizeof(uhdev->desc) - 1));

	if (size > 1) {
		if (vendor) {
			usnprintf(uhdev->desc, size, uL("" uPs("") " " uPs("")), udi->udi_vendor, udi->udi_product);
		} else {
			usnprintf(uhdev->desc, size, uL("" uPs("")), udi->udi_product);
		}
	}
#endif
	uhdev->bustype = udi->udi_bus;
	uhdev->vendor_id = udi->udi_vendorNo;
	uhdev->product_id = udi->udi_productNo;
	uhdev->version = udi->udi_releaseNo;
}
INLINE static void hat_validate(_js_device *jdev, int index, SDBWORD value) {
	_js_axis *jsx = &jdev->data.hat[index];

	if (jsx->used) {
		SDBWORD x, y;

		hat_to_xy(value, &x, &y);
		js_axs_validate(jsx, x);
		jsx = &jdev->data.hat[index + 1];
		js_axs_validate(jsx, y);
	}
}
INLINE static void hat_to_xy(SDBWORD hat, SDBWORD *x, SDBWORD *y) {
	(*x) = (*y) = 0;
	if ((hat & 0x000F) == 0x000F) {
		return;
	}
	if ((hat & 0x07) == hat) {
		switch (hat) {
			case JS_OS_HAT_UP:
				(*y) = JS_AXIS_MIN;
				break;
			case JS_OS_HAT_UP_RIGHT:
				(*y) = JS_AXIS_MIN;
				(*x) = JS_AXIS_MAX;
				break;
			case JS_OS_HAT_RIGHT:
				(*x) = JS_AXIS_MAX;
				break;
			case JS_OS_HAT_RIGHT_DOWN:
				(*x) = JS_AXIS_MAX;
				(*y) = JS_AXIS_MAX;
				break;
			case JS_OS_HAT_DOWN:
				(*y) = JS_AXIS_MAX;
				break;
			case JS_OS_HAT_DOWN_LEFT:
				(*x) = JS_AXIS_MIN;
				(*y) = JS_AXIS_MAX;
				break;
			case JS_OS_HAT_LEFT:
				(*x) = JS_AXIS_MIN;
				break;
			case JS_OS_HAT_LEFT_UP:
				(*x) = JS_AXIS_MIN;
				(*y) = JS_AXIS_MIN;
				break;
		}
	}
}
