/*
 * This file is part of roccat-tools.
 *
 * roccat-tools 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.
 *
 * roccat-tools 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 roccat-tools. If not, see <http://www.gnu.org/licenses/>.
 */

#include "roccat/roccat.h"
#include <g_udev_roccat_helper.h>
#include <string.h>

/*
 * Not using GUdevEnumerator to be backwards compatible as far as possible
 */

struct _RoccatDeviceScanner {
	GUdevClient *client;
	gchar const *driver_name;
	roccat_device_scanner_callback add_callback;
	roccat_device_scanner_callback remove_callback;
	gpointer user_data;
	guint const *device_ids;
};

#define UDEV_ROCCAT_DEVICE_ACTION_ADD "add"
#define UDEV_ROCCAT_DEVICE_ACTION_REMOVE "remove"

static guint usbhid_device_get_interfaceprotocol(GUdevDevice *usbhid_device) {
	return g_udev_device_get_sysfs_attr_as_uint64(usbhid_device, "bInterfaceProtocol");
}

static void fill_input(RoccatDevice *roccat_dev, GUdevDevice *event_device) {
	GUdevDevice *usbhid_device;
	guint protocol;

	usbhid_device = g_udev_roccat_get_parent_usbhid_device(event_device);
	if (!usbhid_device)
		return;

	protocol = usbhid_device_get_interfaceprotocol(usbhid_device);
	g_object_unref(usbhid_device);

	switch(protocol) {
	case ROCCAT_USB_INTERFACE_PROTOCOL_NONE:
		roccat_device_set_none_evdev(roccat_dev, g_udev_device_get_device_file(event_device));
		break;
	case ROCCAT_USB_INTERFACE_PROTOCOL_KEYBOARD:
		roccat_device_set_keyboard_evdev(roccat_dev, g_udev_device_get_device_file(event_device));
		break;
	case ROCCAT_USB_INTERFACE_PROTOCOL_MOUSE:
		roccat_device_set_mouse_evdev(roccat_dev, g_udev_device_get_device_file(event_device));
		break;
	default:
		g_warning("Roccat scan: unknown protocol");
	}
}

static void fill_inputs(RoccatDevice *roccat_dev, GUdevClient *client, GUdevDevice *wanted_usb_device) {
	GUdevDevice *event_device, *usb_device;
	GList *list, *iterator;

	list = g_udev_client_query_by_subsystem(client, "input");
	if (list == NULL)
		return;

	for (iterator = g_list_first(list); iterator; iterator = g_list_next(iterator)) {
		event_device = (GUdevDevice *)iterator->data;
		if (!event_device)
			continue;

		/* take only event* devnodes */
		if (!g_udev_device_get_name(event_device) || strncmp("event", g_udev_device_get_name(event_device), 5))
			continue;

		usb_device = g_udev_roccat_get_parent_usb_device(event_device);
		if (!usb_device)
			continue;

		if (!g_strcmp0(g_udev_device_get_name(usb_device), g_udev_device_get_name(wanted_usb_device)))
			fill_input(roccat_dev, event_device);

		g_object_unref(usb_device);
	}

	g_udev_roccat_device_list_free(list);
}

static void fill_syspath(RoccatDevice *roccat_dev, GUdevDevice *char_device) {
	GUdevDevice *usbhid_device;

	if (!g_strcmp0(g_udev_device_get_subsystem(char_device), "roccat")) {
		usbhid_device = g_udev_roccat_get_parent_usbhid_device(char_device);
		if (!usbhid_device)
			return;
		roccat_device_set_syspath(roccat_dev, g_udev_device_get_sysfs_path(usbhid_device));
		g_object_unref(usbhid_device);
	}
	else
		roccat_device_set_syspath(roccat_dev, g_udev_device_get_sysfs_path(char_device));
}

static void fill_drivername(RoccatDevice *roccat_dev, GUdevDevice *char_device) {
	GUdevDevice *hid_device;

	hid_device = g_udev_roccat_get_parent_hid_device(char_device);
	if (!hid_device)
		return;

	roccat_device_set_drivername(roccat_dev, g_udev_device_get_driver(hid_device));
	g_object_unref(hid_device);
}

static void fill_device_id(RoccatDevice *roccat_dev, GUdevDevice *usb_device) {
	roccat_device_set_product_id(roccat_dev, g_udev_roccat_get_device_id(usb_device));
}

static RoccatDevice *roccat_device_new_from_chardev(GUdevClient *client, GUdevDevice *char_device) {
	GUdevDevice *usb_device;
	RoccatDevice *roccat_dev;

	usb_device = g_udev_roccat_get_parent_usb_device(char_device);
	if (!usb_device)
		return NULL;

	roccat_dev = roccat_device_new(g_udev_device_get_name(usb_device));
	roccat_device_set_roccat_dev(roccat_dev, g_udev_device_get_device_file(char_device));
	fill_drivername(roccat_dev, char_device);
	fill_syspath(roccat_dev, char_device);
	fill_inputs(roccat_dev, client, usb_device);
	fill_device_id(roccat_dev, usb_device);

	g_object_unref(usb_device);

	return roccat_dev;
}

/* returns partially filled device meant for removal event propagation */
static RoccatDevice *roccat_device_new_for_removal(GUdevDevice *usb_device) {
	RoccatDevice *roccat_dev;
	roccat_dev = roccat_device_new(g_udev_device_get_name(usb_device));
	return roccat_dev;
}

static gboolean driver_name_fits(GUdevDevice *device, gchar const *driver_name) {
	GUdevDevice *hid_device;
	int retval;

	hid_device = g_udev_roccat_get_parent_hid_device(device);
	if (!hid_device)
		return FALSE;

	retval = g_strcmp0(g_udev_device_get_driver(hid_device), driver_name);
	g_object_unref(hid_device);
	return (retval == 0) ? TRUE : FALSE;
}

static gboolean device_id_fits(GUdevDevice *device, guint const *device_ids) {
	GUdevDevice *usb_device;
	guint device_id;

	usb_device = g_udev_roccat_get_parent_usb_device(device);
	if (!usb_device)
		return FALSE;

	device_id = g_udev_roccat_get_device_id(usb_device);
	g_object_unref(usb_device);

	while (*device_ids) {
		if (device_id == *device_ids)
			return TRUE;
		++device_ids;
	}
	return FALSE;
}

static gboolean subsystem_fits(GUdevDevice *device, gchar const *driver_name) {
	if (g_strcmp0(g_udev_device_get_subsystem(device), "roccat") &&
			g_strcmp0(g_udev_device_get_subsystem(device), driver_name))
		return FALSE;
	return TRUE;
}

static gboolean device_is_wanted(GUdevDevice *char_device, gchar const *driver_name, guint const *device_ids) {
	if (!subsystem_fits(char_device, driver_name))
		return FALSE;

	if (!driver_name_fits(char_device, driver_name))
		return FALSE;

	if (!device_id_fits(char_device, device_ids))
		return FALSE;

	return TRUE;
}

static void call_add_callback(RoccatDeviceScanner *scanner, GUdevDevice *device) {
	RoccatDevice *roccat_device;

	if (device_is_wanted(device, scanner->driver_name, scanner->device_ids)) {
		if (scanner->add_callback) {
			roccat_device = roccat_device_new_from_chardev(scanner->client, device);
			if (!roccat_device)
				return;
			if (scanner->add_callback(roccat_device, scanner->user_data))
				roccat_device_free(roccat_device);
		}
	}
}

static void uevent_add_cb(GUdevClient *client, gchar *action, GUdevDevice *device, RoccatDeviceScanner *scanner) {
	if (g_strcmp0(action, UDEV_ROCCAT_DEVICE_ACTION_ADD) == 0)
		call_add_callback(scanner, device);
}

static void uevent_remove_cb(GUdevClient *client, gchar *action, GUdevDevice *device, RoccatDeviceScanner *scanner) {
	RoccatDevice *roccat_device;

	if (g_strcmp0(g_udev_device_get_subsystem(device), "usb") == 0 &&
			g_strcmp0(g_udev_device_get_devtype(device), "usb_device") == 0 &&
			g_strcmp0(action, UDEV_ROCCAT_DEVICE_ACTION_REMOVE) == 0) {
		/* device is usb_device */
		if (scanner->remove_callback) {
			roccat_device = roccat_device_new_for_removal(device);
			if (scanner->remove_callback(roccat_device, scanner->user_data))
				roccat_device_free(roccat_device);
		}
	}
}

static GList *client_query_by_subsystems(GUdevClient *client, gchar const *driver_name) {
	/*
	 * Newer driver versions (>= 2.6.38) have char device in subsystem
	 * with the same name than the driver, and the syfs attributes in char devs folder.
	 * Fallback: Old driver versions have the char device in subsystem roccat
	 * and the sysfs attributes in usbhid path.
	 */
	return g_list_concat(g_udev_client_query_by_subsystem(client, driver_name),
			g_udev_client_query_by_subsystem(client, "roccat"));
}

static void scanner_call_callback_for_all_wanted(RoccatDeviceScanner *scanner) {
	GList *list;
	GList *iterator;

	list = client_query_by_subsystems(scanner->client, scanner->driver_name);

	for (iterator = g_list_first(list); iterator; iterator = g_list_next(iterator))
		call_add_callback(scanner, (GUdevDevice *)iterator->data);

	g_udev_roccat_device_list_free(list);
}

RoccatDeviceScanner *roccat_device_scanner_new(gchar const *driver_name, guint const *device_ids,
		roccat_device_scanner_callback add_callback, roccat_device_scanner_callback remove_callback,
		gpointer user_data) {
	RoccatDeviceScanner *scanner;
	gchar const * const subsystems[] = { "usb/usb_device", "roccat", driver_name, NULL };

	scanner = (RoccatDeviceScanner *)g_malloc0(sizeof(RoccatDeviceScanner));

	scanner->client = g_udev_client_new(subsystems);
	scanner->driver_name = driver_name;
	scanner->add_callback = add_callback;
	scanner->remove_callback = remove_callback;
	scanner->user_data = user_data;
	scanner->device_ids = device_ids;

	g_signal_connect(G_OBJECT(scanner->client), "uevent", G_CALLBACK(uevent_add_cb), scanner);
	g_signal_connect(G_OBJECT(scanner->client), "uevent", G_CALLBACK(uevent_remove_cb), scanner);

	scanner_call_callback_for_all_wanted(scanner);

	return scanner;
}

void roccat_device_scanner_free(RoccatDeviceScanner *scanner) {
	g_return_if_fail(scanner != NULL);
	g_object_unref(scanner->client);
	g_free(scanner);
}

static RoccatDevice *device_list_get_first_wanted(GList *list, GUdevClient *client, gchar const *driver_name, guint const *device_ids) {
	GList *iterator;
	GUdevDevice *char_device;

	for (iterator = g_list_first(list); iterator; iterator = g_list_next(iterator)) {
		char_device = (GUdevDevice *)iterator->data;

		if (device_is_wanted(char_device, driver_name, device_ids))
			return roccat_device_new_from_chardev(client, char_device);
	}

	return NULL;
}

static RoccatDevice *client_get_first_wanted(GUdevClient *client, gchar const *driver_name, guint const *device_ids) {
	GList *list;
	RoccatDevice *roccat_device;

	list = client_query_by_subsystems(client, driver_name);
	roccat_device = device_list_get_first_wanted(list, client, driver_name, device_ids);
	g_udev_roccat_device_list_free(list);

	return roccat_device;
}

RoccatDevice *roccat_device_first(gchar const *driver_name, guint const *device_ids) {
	GUdevClient *client;
	RoccatDevice *roccat_device;

	client = g_udev_client_new(NULL);
	roccat_device = client_get_first_wanted(client, driver_name, device_ids);
	g_object_unref(client);

	return roccat_device;
}
