/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2016-2017 Richard Hughes <richard@hughsie.com>
 *
 * Licensed under the GNU Lesser General Public License Version 2.1
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
 */

#include "config.h"

#include <fcntl.h>
#include <glib/gstdio.h>
#include <string.h>
#include <appstream-glib.h>

#include "lu-common.h"
#include "lu-device-bootloader-nordic.h"
#include "lu-device-bootloader-texas.h"
#include "lu-device.h"
#include "lu-device-runtime.h"
#include "lu-hidpp.h"

typedef struct
{
	LuDeviceKind		 kind;
	GUdevDevice		*udev_device;
	gint			 udev_device_fd;
	GUsbDevice		*usb_device;
	FuDeviceLocker		*usb_device_locker;
	gchar			*version_hw;
	LuDeviceFlags		 flags;
	guint8			 hidpp_id;
	guint8			 battery_level;
	gdouble			 hidpp_version;
	GPtrArray		*feature_index;
} LuDevicePrivate;

typedef struct {
	guint8			 idx;
	guint16			 feature;
} LuDeviceHidppMap;

G_DEFINE_TYPE_WITH_PRIVATE (LuDevice, lu_device, FU_TYPE_DEVICE)

enum {
	PROP_0,
	PROP_KIND,
	PROP_HIDPP_ID,
	PROP_FLAGS,
	PROP_UDEV_DEVICE,
	PROP_USB_DEVICE,
	PROP_LAST
};

#define GET_PRIVATE(o) (lu_device_get_instance_private (o))

LuDeviceKind
lu_device_kind_from_string (const gchar *kind)
{
	if (g_strcmp0 (kind, "runtime") == 0)
		return LU_DEVICE_KIND_RUNTIME;
	if (g_strcmp0 (kind, "bootloader-nordic") == 0)
		return LU_DEVICE_KIND_BOOTLOADER_NORDIC;
	if (g_strcmp0 (kind, "bootloader-texas") == 0)
		return LU_DEVICE_KIND_BOOTLOADER_TEXAS;
	if (g_strcmp0 (kind, "peripheral") == 0)
		return LU_DEVICE_KIND_PERIPHERAL;
	return LU_DEVICE_KIND_UNKNOWN;
}

const gchar *
lu_device_kind_to_string (LuDeviceKind kind)
{
	if (kind == LU_DEVICE_KIND_RUNTIME)
		return "runtime";
	if (kind == LU_DEVICE_KIND_BOOTLOADER_NORDIC)
		return "bootloader-nordic";
	if (kind == LU_DEVICE_KIND_BOOTLOADER_TEXAS)
		return "bootloader-texas";
	if (kind == LU_DEVICE_KIND_PERIPHERAL)
		return "peripheral";
	return NULL;
}

static const gchar *
lu_hidpp_feature_to_string (guint16 feature)
{
	if (feature == HIDPP_FEATURE_ROOT)
		return "Root";
	if (feature == HIDPP_FEATURE_I_FIRMWARE_INFO)
		return "IFirmwareInfo";
	if (feature == HIDPP_FEATURE_GET_DEVICE_NAME_TYPE)
		return "GetDevicenameType";
	if (feature == HIDPP_FEATURE_BATTERY_LEVEL_STATUS)
		return "BatteryLevelStatus";
	if (feature == HIDPP_FEATURE_DFU_CONTROL)
		return "DfuControl";
	if (feature == HIDPP_FEATURE_DFU_CONTROL_SIGNED)
		return "DfuControlSigned";
	if (feature == HIDPP_FEATURE_DFU)
		return "Dfu";
	return NULL;
}

static gchar *
lu_device_flags_to_string (LuDeviceFlags flags)
{
	GString *str = g_string_new (NULL);
	if (flags & LU_DEVICE_FLAG_REQUIRES_SIGNED_FIRMWARE)
		g_string_append (str, "signed-firmware,");
	if (flags & LU_DEVICE_FLAG_REQUIRES_RESET)
		g_string_append (str, "requires-reset,");
	if (flags & LU_DEVICE_FLAG_ACTIVE)
		g_string_append (str, "active,");
	if (flags & LU_DEVICE_FLAG_IS_OPEN)
		g_string_append (str, "is-open,");
	if (flags & LU_DEVICE_FLAG_REQUIRES_ATTACH)
		g_string_append (str, "requires-attach,");
	if (flags & LU_DEVICE_FLAG_REQUIRES_DETACH)
		g_string_append (str, "requires-detach,");
	if (flags & LU_DEVICE_FLAG_DETACH_WILL_REPLUG)
		g_string_append (str, "detach-will-replug,");
	if (str->len == 0) {
		g_string_append (str, "none");
	} else {
		g_string_truncate (str, str->len - 1);
	}
	return g_string_free (str, FALSE);
}

static void
lu_device_to_string (FuDevice *device, GString *str)
{
	LuDevice *self = LU_DEVICE (device);
	LuDevicePrivate *priv = GET_PRIVATE (self);
	g_autofree gchar *flags_str = NULL;

	g_string_append_printf (str, "  Type:\t\t\t%s\n", lu_device_kind_to_string (priv->kind));
	flags_str = lu_device_flags_to_string (priv->flags);
	g_string_append_printf (str, "  Flags:\t\t%s\n", flags_str);
	g_string_append_printf (str, "  HidppVersion:\t\t%.2f\n", priv->hidpp_version);
	if (priv->hidpp_id != HIDPP_DEVICE_ID_UNSET)
		g_string_append_printf (str, "  HidppId:\t\t0x%02x\n", (guint) priv->hidpp_id);
	if (priv->udev_device_fd > 0)
		g_string_append_printf (str, "  UdevDevice:\t\t%i\n", priv->udev_device_fd);
	if (priv->usb_device != NULL)
		g_string_append_printf (str, "  UsbDevice:\t\t%p\n", priv->usb_device);
	if (priv->version_hw != NULL)
		g_string_append_printf (str, "  VersionHardware:\t%s\n", priv->version_hw);
	if (priv->battery_level != 0)
		g_string_append_printf (str, "  Battery-level:\t\t%u\n", priv->battery_level);
	for (guint i = 0; i < priv->feature_index->len; i++) {
		LuDeviceHidppMap *map = g_ptr_array_index (priv->feature_index, i);
		g_string_append_printf (str, "  Feature%02x:\t\t%s [0x%04x]\n",
					map->idx,
					lu_hidpp_feature_to_string (map->feature),
					map->feature);
	}

	/* fixme: superclass? */
	if (LU_IS_DEVICE_BOOTLOADER (device)) {
		g_string_append_printf (str, "  FlashAddrHigh:\t0x%04x\n",
					lu_device_bootloader_get_addr_hi (self));
		g_string_append_printf (str, "  FlashAddrLow:\t0x%04x\n",
					lu_device_bootloader_get_addr_lo (self));
		g_string_append_printf (str, "  FlashBlockSize:\t0x%04x\n",
					lu_device_bootloader_get_blocksize (self));
	}
}

guint8
lu_device_hidpp_feature_get_idx (LuDevice *device, guint16 feature)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	for (guint i = 0; i < priv->feature_index->len; i++) {
		LuDeviceHidppMap *map = g_ptr_array_index (priv->feature_index, i);
		if (map->feature == feature)
			return map->idx;
	}
	return 0x00;
}

guint16
lu_device_hidpp_feature_find_by_idx (LuDevice *device, guint8 idx)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	for (guint i = 0; i < priv->feature_index->len; i++) {
		LuDeviceHidppMap *map = g_ptr_array_index (priv->feature_index, i);
		if (map->idx == idx)
			return map->feature;
	}
	return 0x0000;
}

static void
lu_device_hidpp_dump (LuDevice *device, const gchar *title, const guint8 *data, gsize len)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	g_autofree gchar *title_prefixed = NULL;
	if (priv->usb_device != NULL)
		title_prefixed = g_strdup_printf ("[USB] %s", title);
	else if (priv->udev_device != NULL)
		title_prefixed = g_strdup_printf ("[HID] %s", title);
	else
		title_prefixed = g_strdup_printf ("[EMU] %s", title);
	lu_dump_raw (title_prefixed, data, len);
}

static const gchar *
lu_device_hidpp20_function_to_string (guint16 feature, guint8 function_id)
{
	if (feature == HIDPP_FEATURE_ROOT) {
		if (function_id == 0x00)
			return "getFeature";
		if (function_id == 0x01)
			return "ping";
		return NULL;
	}
	if (feature == HIDPP_FEATURE_I_FIRMWARE_INFO) {
		if (function_id == 0x00)
			return "getCount";
		if (function_id == 0x01)
			return "getInfo";
		return NULL;
	}
	if (feature == HIDPP_FEATURE_BATTERY_LEVEL_STATUS) {
		if (function_id == 0x00)
			return "GetBatteryLevelStatus";
		return NULL;
	}
	if (feature == HIDPP_FEATURE_DFU_CONTROL) {
		if (function_id == 0x00)
			return "getDfuControl";
		if (function_id == 0x01)
			return "setDfuControl";
		return NULL;
	}
	if (feature == HIDPP_FEATURE_DFU_CONTROL_SIGNED) {
		if (function_id == 0x00)
			return "getDfuStatus";
		if (function_id == 0x01)
			return "startDfu";
		return NULL;
	}
	if (feature == HIDPP_FEATURE_DFU) {
		if (function_id == 0x00)
			return "dfuCmdData0";
		if (function_id == 0x01)
			return "dfuCmdData1";
		if (function_id == 0x02)
			return "dfuCmdData2";
		if (function_id == 0x03)
			return "dfuCmdData3";
		if (function_id == 0x04)
			return "dfuStart";
		if (function_id == 0x05)
			return "restart";
		return NULL;
	}
	return NULL;
}

static gchar *
lu_device_hidpp_msg_to_string (LuDevice *device, LuHidppMsg *msg)
{
	GString *str = g_string_new (NULL);
	LuDevicePrivate *priv = GET_PRIVATE (device);
	const gchar *tmp;
	const gchar *kind_str = lu_device_kind_to_string (priv->kind);
	g_autoptr(GError) error = NULL;
	g_autoptr(GString) flags_str = g_string_new (NULL);

	g_return_val_if_fail (msg != NULL, NULL);

	g_string_append_printf (str, "device-kind: %s\n", kind_str);
	if (msg->flags == LU_HIDPP_MSG_FLAG_NONE) {
		g_string_append (flags_str, "none");
	} else {
		if (msg->flags & LU_HIDPP_MSG_FLAG_LONGER_TIMEOUT)
			g_string_append (flags_str, "longer-timeout,");
		if (msg->flags & LU_HIDPP_MSG_FLAG_IGNORE_SUB_ID)
			g_string_append (flags_str, "ignore-sub-id,");
		if (msg->flags & LU_HIDPP_MSG_FLAG_IGNORE_FNCT_ID)
			g_string_append (flags_str, "ignore-fnct-id,");
		if (msg->flags & LU_HIDPP_MSG_FLAG_IGNORE_SWID)
			g_string_append (flags_str, "ignore-swid,");
		if (str->len > 0)
			g_string_truncate (str, str->len - 1);
	}
	g_string_append_printf (str, "flags:       %02x   [%s]\n",
				msg->flags,
				flags_str->str);
	g_string_append_printf (str, "report-id:   %02x   [%s]\n",
				msg->report_id,
				lu_hidpp_msg_rpt_id_to_string (msg));
	tmp = lu_hidpp_msg_dev_id_to_string (msg);
	g_string_append_printf (str, "device-id:   %02x   [%s]\n",
				msg->device_id, tmp );
	if (priv->hidpp_version >= 2.f) {
		guint16 feature = lu_device_hidpp_feature_find_by_idx (device, msg->sub_id);
		guint8 sw_id = msg->function_id & 0x0f;
		guint8 function_id = (msg->function_id & 0xf0) >> 4;
		g_string_append_printf (str, "feature:     %04x [%s]\n",
					feature,
					lu_hidpp_feature_to_string (feature));
		g_string_append_printf (str, "function-id: %02x   [%s]\n",
					function_id,
					lu_device_hidpp20_function_to_string (feature, function_id));
		g_string_append_printf (str, "sw-id:       %02x   [%s]\n",
					sw_id,
					sw_id == LU_HIDPP_MSG_SW_ID ? "fwupd" : "???");
	} else {
		g_string_append_printf (str, "sub-id:      %02x   [%s]\n",
					msg->sub_id,
					lu_hidpp_msg_sub_id_to_string (msg));
		g_string_append_printf (str, "function-id: %02x   [%s]\n",
					msg->function_id,
					lu_hidpp_msg_fcn_id_to_string (msg));
	}
	if (!lu_hidpp_msg_is_error (msg, &error)) {
		g_string_append_printf (str, "error:       %s\n",
					error->message);
	}
	return g_string_free (str, FALSE);
}

gboolean
lu_device_hidpp_send (LuDevice *device,
		      LuHidppMsg *msg,
		      guint timeout,
		      GError **error)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	gsize len = lu_hidpp_msg_get_payload_length (msg);

	/* only for HID++2.0 */
	if (lu_device_get_hidpp_version (device) >= 2.f)
		msg->function_id |= LU_HIDPP_MSG_SW_ID;

	lu_device_hidpp_dump (device, "host->device", (guint8 *) msg, len);

	/* detailed debugging */
	if (g_getenv ("FWUPD_UNIFYING_VERBOSE") != NULL) {
		g_autofree gchar *str = lu_device_hidpp_msg_to_string (device, msg);
		g_print ("%s", str);
	}

	/* USB */
	if (priv->usb_device != NULL) {
		gsize actual_length = 0;
		if (!g_usb_device_control_transfer (priv->usb_device,
						    G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
						    G_USB_DEVICE_REQUEST_TYPE_CLASS,
						    G_USB_DEVICE_RECIPIENT_INTERFACE,
						    LU_REQUEST_SET_REPORT,
						    0x0210, 0x0002,
						    (guint8 *) msg, len,
						    &actual_length,
						    timeout,
						    NULL,
						    error)) {
			g_prefix_error (error, "failed to send data: ");
			return FALSE;
		}
		if (actual_length != len) {
			g_set_error (error,
				     G_IO_ERROR,
				     G_IO_ERROR_FAILED,
				     "failed to send data: "
				     "wrote %" G_GSIZE_FORMAT " of %" G_GSIZE_FORMAT,
				     actual_length, len);
			return FALSE;
		}

	/* HID */
	} else if (priv->udev_device != NULL) {
		if (!lu_nonblock_write (priv->udev_device_fd,
					(guint8 *) msg, len, error)) {
			g_prefix_error (error, "failed to send: ");
			return FALSE;
		}
	}

	/* success */
	return TRUE;
}

gboolean
lu_device_hidpp_receive (LuDevice *device,
			 LuHidppMsg *msg,
			 guint timeout,
			 GError **error)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	gsize read_size = 0;

	/* USB */
	if (priv->usb_device != NULL) {
		if (!g_usb_device_interrupt_transfer (priv->usb_device,
						      LU_DEVICE_EP3,
						      (guint8 *) msg,
						      sizeof(LuHidppMsg),
						      &read_size,
						      timeout,
						      NULL,
						      error)) {
			g_prefix_error (error, "failed to get data: ");
			return FALSE;
		}

	/* HID */
	} else if (priv->udev_device != NULL) {
		if (!lu_nonblock_read (priv->udev_device_fd,
				       (guint8 *) msg,
				       sizeof(LuHidppMsg),
				       &read_size,
				       timeout,
				       error)) {
			g_prefix_error (error, "failed to receive: ");
			return FALSE;
		}
	}

	/* check long enough, but allow returning oversize packets */
	lu_device_hidpp_dump (device, "device->host", (guint8 *) msg, read_size);
	if (read_size < lu_hidpp_msg_get_payload_length (msg)) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_FAILED,
			     "message length too small, "
			     "got %" G_GSIZE_FORMAT " expected %" G_GSIZE_FORMAT,
			     read_size, lu_hidpp_msg_get_payload_length (msg));
		return FALSE;
	}

	/* detailed debugging */
	if (g_getenv ("FWUPD_UNIFYING_VERBOSE") != NULL) {
		g_autofree gchar *str = lu_device_hidpp_msg_to_string (device, msg);
		g_print ("%s", str);
	}

	/* success */
	return TRUE;
}

gboolean
lu_device_hidpp_transfer (LuDevice *device, LuHidppMsg *msg, GError **error)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	guint timeout = LU_DEVICE_TIMEOUT_MS;
	g_autoptr(LuHidppMsg) msg_tmp = lu_hidpp_msg_new ();

	/* increase timeout for some operations */
	if (msg->flags & LU_HIDPP_MSG_FLAG_LONGER_TIMEOUT)
		timeout *= 10;

	/* send request */
	if (!lu_device_hidpp_send (device, msg, timeout, error))
		return FALSE;

	/* keep trying to receive until we get a valid reply */
	while (1) {
		if (!lu_device_hidpp_receive (device, msg_tmp, timeout, error))
			return FALSE;

		/* we don't know how to handle this report packet */
		if (lu_hidpp_msg_get_payload_length (msg_tmp) == 0x0) {
			g_debug ("HID++1.0 report 0x%02x has unknown length, ignoring",
				 msg_tmp->report_id);
			continue;
		}

		if (!lu_hidpp_msg_is_error (msg_tmp, error))
			return FALSE;

		/* is valid reply */
		if (lu_hidpp_msg_is_reply (msg, msg_tmp))
			break;

		/* to ensure compatibility when an HID++ 2.0 device is
		 * connected to an HID++ 1.0 receiver, any feature index
		 * corresponding to an HID++ 1.0 sub-identifier which could be
		 * sent by the receiver, must be assigned to a dummy feature */
		if (lu_device_get_hidpp_version (device) >= 2.f) {
			if (lu_hidpp_msg_is_hidpp10_compat (msg_tmp)) {
				g_debug ("ignoring HID++1.0 reply");
				continue;
			}

			/* not us */
			if ((msg->flags & LU_HIDPP_MSG_FLAG_IGNORE_SWID) == 0) {
				if (!lu_hidpp_msg_verify_swid (msg_tmp)) {
					g_debug ("ignoring reply with SwId 0x%02i, expected 0x%02i",
						 msg_tmp->function_id & 0x0f,
						 LU_HIDPP_MSG_SW_ID);
					continue;
				}
			}
		}

		g_debug ("ignoring message");

	};

	/* if the HID++ ID is unset, grab it from the reply */
	if (priv->hidpp_id == HIDPP_DEVICE_ID_UNSET) {
		priv->hidpp_id = msg_tmp->device_id;
		g_debug ("HID++ ID now %02x", priv->hidpp_id);
	}

	/* copy over data */
	lu_hidpp_msg_copy (msg, msg_tmp);
	return TRUE;
}

gboolean
lu_device_hidpp_feature_search (LuDevice *device, guint16 feature, GError **error)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	LuDeviceHidppMap *map;
	g_autoptr(LuHidppMsg) msg = lu_hidpp_msg_new ();

	/* find the idx for the feature */
	msg->report_id = HIDPP_REPORT_ID_SHORT;
	msg->device_id = priv->hidpp_id;
	msg->sub_id = 0x00; /* rootIndex */
	msg->function_id = 0x00 << 4; /* getFeature */
	msg->data[0] = feature >> 8;
	msg->data[1] = feature;
	msg->data[2] = 0x00;
	if (!lu_device_hidpp_transfer (device, msg, error)) {
		g_prefix_error (error,
				"failed to get idx for feature %s [0x%04x]: ",
				lu_hidpp_feature_to_string (feature), feature);
		return FALSE;
	}

	/* zero index */
	if (msg->data[0] == 0x00) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_NOT_SUPPORTED,
			     "feature %s [0x%04x] not found",
			     lu_hidpp_feature_to_string (feature), feature);
		return FALSE;
	}

	/* add to map */
	map = g_new0 (LuDeviceHidppMap, 1);
	map->idx = msg->data[0];
	map->feature = feature;
	g_ptr_array_add (priv->feature_index, map);
	g_debug ("added feature %s [0x%04x] as idx %02x",
		 lu_hidpp_feature_to_string (feature), feature, map->idx);
	return TRUE;
}

LuDeviceKind
lu_device_get_kind (LuDevice *device)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	return priv->kind;
}

guint8
lu_device_get_hidpp_id (LuDevice *device)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	return priv->hidpp_id;
}

void
lu_device_set_hidpp_id (LuDevice *device, guint8 hidpp_id)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	priv->hidpp_id = hidpp_id;
}

guint8
lu_device_get_battery_level (LuDevice *device)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	return priv->battery_level;
}

void
lu_device_set_battery_level (LuDevice *device, guint8 percentage)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	priv->battery_level = percentage;
}

gdouble
lu_device_get_hidpp_version (LuDevice *device)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	return priv->hidpp_version;
}

void
lu_device_set_hidpp_version (LuDevice *device, gdouble hidpp_version)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	priv->hidpp_version = hidpp_version;
}

const gchar *
lu_device_get_version_hw (LuDevice *device)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	return priv->version_hw;
}

void
lu_device_set_version_hw (LuDevice *device, const gchar *version_hw)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	g_free (priv->version_hw);
	priv->version_hw = g_strdup (version_hw);
}

gboolean
lu_device_has_flag (LuDevice *device, LuDeviceFlags flag)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	return (priv->flags & flag) > 0;
}

void
lu_device_add_flag (LuDevice *device, LuDeviceFlags flag)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	priv->flags |= flag;
	g_object_notify (G_OBJECT (device), "flags");
}

void
lu_device_remove_flag (LuDevice *device, LuDeviceFlags flag)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	priv->flags &= ~flag;
	g_object_notify (G_OBJECT (device), "flags");
}

LuDeviceFlags
lu_device_get_flags (LuDevice *device)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	return priv->flags;
}

GUdevDevice *
lu_device_get_udev_device (LuDevice *device)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	return priv->udev_device;
}

GUsbDevice *
lu_device_get_usb_device (LuDevice *device)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	return priv->usb_device;
}

gboolean
lu_device_probe (LuDevice *device, GError **error)
{
	LuDeviceClass *klass = LU_DEVICE_GET_CLASS (device);
	LuDevicePrivate *priv = GET_PRIVATE (device);

	/* clear the feature map (leaving only the root) */
	g_ptr_array_set_size (priv->feature_index, 0);

	/* probe the hardware */
	if (klass->probe != NULL)
		return klass->probe (device, error);
	return TRUE;
}

gboolean
lu_device_open (LuDevice *device, GError **error)
{
	LuDeviceClass *klass = LU_DEVICE_GET_CLASS (device);
	LuDevicePrivate *priv = GET_PRIVATE (device);
	g_autofree gchar *device_str = NULL;

	g_return_val_if_fail (LU_IS_DEVICE (device), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	/* already done */
	if (lu_device_has_flag (device, LU_DEVICE_FLAG_IS_OPEN))
		return TRUE;

	/* set default vendor */
	fu_device_set_vendor (FU_DEVICE (device), "Logitech");

	/* USB */
	if (priv->usb_device != NULL) {
		guint8 num_interfaces = 0x01;
		g_autofree gchar *devid = NULL;

		/* open device */
		if (priv->usb_device_locker == NULL) {
			g_autoptr(FuDeviceLocker) locker = NULL;
			g_debug ("opening unifying device using USB");
			locker = fu_device_locker_new (priv->usb_device, error);
			if (locker == NULL)
				return FALSE;
			if (priv->kind == LU_DEVICE_KIND_RUNTIME)
				num_interfaces = 0x03;
			for (guint i = 0; i < num_interfaces; i++) {
				g_debug ("claiming interface 0x%02x", i);
				if (!g_usb_device_claim_interface (priv->usb_device, i,
								   G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
								   error)) {
					g_prefix_error (error, "Failed to claim 0x%02x: ", i);
					return FALSE;
				}
			}
			priv->usb_device_locker = g_steal_pointer (&locker);
		}

		/* generate GUID */
		devid = g_strdup_printf ("USB\\VID_%04X&PID_%04X",
					 g_usb_device_get_vid (priv->usb_device),
					 g_usb_device_get_pid (priv->usb_device));
		fu_device_add_guid (FU_DEVICE (device), devid);

	/* HID */
	} else if (priv->udev_device != NULL) {
		const gchar *devpath = g_udev_device_get_device_file (priv->udev_device);
		g_debug ("opening unifying device using %s", devpath);
		priv->udev_device_fd = lu_nonblock_open (devpath, error);
		if (priv->udev_device_fd < 0)
			return FALSE;
	}

	/* subclassed */
	if (klass->open != NULL) {
		if (!klass->open (device, error)) {
			lu_device_close (device, NULL);
			return FALSE;
		}
	}
	lu_device_add_flag (device, LU_DEVICE_FLAG_IS_OPEN);

	/* subclassed */
	if (!lu_device_probe (device, error)) {
		lu_device_close (device, NULL);
		return FALSE;
	}

	/* add known root for HID++2.0 */
	if (lu_device_get_hidpp_version (device) >= 2.f) {
		LuDeviceHidppMap *map = g_new0 (LuDeviceHidppMap, 1);
		map->idx = 0x00;
		map->feature = HIDPP_FEATURE_ROOT;
		g_ptr_array_add (priv->feature_index, map);
	}

	/* show the device */
	device_str = fu_device_to_string (FU_DEVICE (device));
	g_debug ("%s", device_str);

	/* success */
	return TRUE;
}

gboolean
lu_device_poll (LuDevice *device, GError **error)
{
	LuDeviceClass *klass = LU_DEVICE_GET_CLASS (device);
	if (klass->poll != NULL) {
		if (!klass->poll (device, error))
			return FALSE;
	}
	return TRUE;
}

/**
 * lu_device_close:
 * @device: A #LuDevice
 * @error: A #GError, or %NULL
 *
 * Closes the device.
 *
 * Returns: %TRUE for success
 **/
gboolean
lu_device_close (LuDevice *device, GError **error)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	LuDeviceClass *klass = LU_DEVICE_GET_CLASS (device);

	g_return_val_if_fail (LU_IS_DEVICE (device), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	/* not open */
	if (!lu_device_has_flag (device, LU_DEVICE_FLAG_IS_OPEN))
		return TRUE;

	/* subclassed */
	g_debug ("closing device");
	if (klass->close != NULL) {
		if (!klass->close (device, error))
			return FALSE;
	}

	/* USB */
	if (priv->usb_device_locker != NULL) {
		guint8 num_interfaces = 0x01;
		if (priv->kind == LU_DEVICE_KIND_RUNTIME)
			num_interfaces = 0x03;
		for (guint i = 0; i < num_interfaces; i++) {
			g_autoptr(GError) error_local = NULL;
			g_debug ("releasing interface 0x%02x", i);
			if (!g_usb_device_release_interface (priv->usb_device, i,
							     G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
							     &error_local)) {
				if (!g_error_matches (error_local,
						      G_USB_DEVICE_ERROR,
						      G_USB_DEVICE_ERROR_INTERNAL)) {
					g_set_error (error,
						     G_IO_ERROR,
						     G_IO_ERROR_FAILED,
						     "Failed to release 0x%02x: %s",
						     i, error_local->message);
					return FALSE;
				}
			}
		}
		g_clear_object (&priv->usb_device_locker);
	}
	g_clear_object (&priv->usb_device);

	/* HID */
	if (priv->udev_device != NULL && priv->udev_device_fd > 0) {
		if (!g_close (priv->udev_device_fd, error))
			return FALSE;
		priv->udev_device_fd = 0;
	}

	/* success */
	lu_device_remove_flag (device, LU_DEVICE_FLAG_IS_OPEN);
	return TRUE;
}
gboolean
lu_device_detach (LuDevice *device, GError **error)
{
	LuDeviceClass *klass = LU_DEVICE_GET_CLASS (device);

	g_return_val_if_fail (LU_IS_DEVICE (device), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	/* subclassed */
	g_debug ("detaching device");
	if (klass->detach != NULL)
		return klass->detach (device, error);

	/* nothing to do */
	g_set_error_literal (error,
			     G_IO_ERROR,
			     G_IO_ERROR_FAILED,
			     "device detach is not supported");
	return FALSE;
}

gboolean
lu_device_attach (LuDevice *device, GError **error)
{
	LuDeviceClass *klass = LU_DEVICE_GET_CLASS (device);

	g_return_val_if_fail (LU_IS_DEVICE (device), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	/* check kind */
	if (lu_device_get_kind (device) == LU_DEVICE_KIND_RUNTIME) {
		g_set_error_literal (error,
				     G_IO_ERROR,
				     G_IO_ERROR_FAILED,
				     "device is not in bootloader state");
		return FALSE;
	}

	/* subclassed */
	if (klass->attach != NULL)
		return klass->attach (device, error);

	return TRUE;
}

gboolean
lu_device_write_firmware (LuDevice *device, GBytes *fw, GError **error)
{
	LuDeviceClass *klass = LU_DEVICE_GET_CLASS (device);

	g_return_val_if_fail (LU_IS_DEVICE (device), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	/* corrupt */
	if (g_bytes_get_size (fw) < 0x4000) {
		g_set_error_literal (error,
				     G_IO_ERROR,
				     G_IO_ERROR_INVALID_DATA,
				     "firmware is too small");
		return FALSE;
	}

	/* call device-specific method */
	if (klass->write_firmware == NULL) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_FAILED,
			     "not supported in %s",
			     lu_device_kind_to_string (lu_device_get_kind (device)));
		return FALSE;
	}

	/* call either nordic or texas vfunc */
	return klass->write_firmware (device, fw, error);
}

#ifndef HAVE_GUDEV_232
G_DEFINE_AUTOPTR_CLEANUP_FUNC(GUdevDevice, g_object_unref)
G_DEFINE_AUTOPTR_CLEANUP_FUNC(GUdevClient, g_object_unref)
#endif

static GUdevDevice *
lu_device_find_udev_device (GUsbDevice *usb_device)
{
	g_autoptr(GUdevClient) gudev_client = g_udev_client_new (NULL);
	g_autoptr(GList) devices = NULL;

	devices = g_udev_client_query_by_subsystem (gudev_client, "usb");
	for (GList *l = devices; l != NULL; l = l->next) {
		guint busnum;
		guint devnum;
		g_autoptr(GUdevDevice) udev_device = G_UDEV_DEVICE (l->data);
		g_autoptr(GUdevDevice) udev_parent = g_udev_device_get_parent (udev_device);

		busnum = g_udev_device_get_sysfs_attr_as_int (udev_parent, "busnum");
		if (busnum != g_usb_device_get_bus (usb_device))
			continue;
		devnum = g_udev_device_get_sysfs_attr_as_int (udev_parent, "devnum");
		if (devnum != g_usb_device_get_address (usb_device))
			continue;

		return g_object_ref (udev_parent);
	}
	return NULL;
}

static void
lu_device_update_platform_id (LuDevice *device)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	if (priv->usb_device != NULL && priv->udev_device == NULL) {
		g_autoptr(GUdevDevice) udev_device = NULL;
		udev_device = lu_device_find_udev_device (priv->usb_device);
		if (udev_device != NULL) {
			const gchar *tmp = g_udev_device_get_sysfs_path (udev_device);
			fu_device_set_platform_id (FU_DEVICE (device), tmp);
		}
	}
}

static void
lu_device_get_property (GObject *object, guint prop_id,
			GValue *value, GParamSpec *pspec)
{
	LuDevice *device = LU_DEVICE (object);
	LuDevicePrivate *priv = GET_PRIVATE (device);
	switch (prop_id) {
	case PROP_KIND:
		g_value_set_uint (value, priv->kind);
		break;
	case PROP_HIDPP_ID:
		g_value_set_uint (value, priv->hidpp_id);
		break;
	case PROP_FLAGS:
		g_value_set_uint64 (value, priv->flags);
		break;
	case PROP_UDEV_DEVICE:
		g_value_set_object (value, priv->udev_device);
		break;
	case PROP_USB_DEVICE:
		g_value_set_object (value, priv->usb_device);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
lu_device_set_property (GObject *object, guint prop_id,
			const GValue *value, GParamSpec *pspec)
{
	LuDevice *device = LU_DEVICE (object);
	LuDevicePrivate *priv = GET_PRIVATE (device);
	switch (prop_id) {
	case PROP_KIND:
		priv->kind = g_value_get_uint (value);
		break;
	case PROP_HIDPP_ID:
		priv->hidpp_id = g_value_get_uint (value);
		break;
	case PROP_FLAGS:
		priv->flags = g_value_get_uint64 (value);
		break;
	case PROP_UDEV_DEVICE:
		priv->udev_device = g_value_dup_object (value);
		break;
	case PROP_USB_DEVICE:
		priv->usb_device = g_value_dup_object (value);
		lu_device_update_platform_id (device);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
lu_device_finalize (GObject *object)
{
	LuDevice *device = LU_DEVICE (object);
	LuDevicePrivate *priv = GET_PRIVATE (device);

	if (priv->usb_device != NULL)
		g_object_unref (priv->usb_device);
	if (priv->usb_device_locker != NULL)
		g_object_unref (priv->usb_device_locker);
	if (priv->udev_device != NULL)
		g_object_unref (priv->udev_device);
	g_ptr_array_unref (priv->feature_index);
	g_free (priv->version_hw);

	G_OBJECT_CLASS (lu_device_parent_class)->finalize (object);
}

static void
lu_device_init (LuDevice *device)
{
	LuDevicePrivate *priv = GET_PRIVATE (device);
	priv->hidpp_id = HIDPP_DEVICE_ID_UNSET;
	priv->feature_index = g_ptr_array_new_with_free_func (g_free);
	fu_device_set_vendor_id (FU_DEVICE (device), "USB:0x046D");
}

static void
lu_device_class_init (LuDeviceClass *klass)
{
	GParamSpec *pspec;
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass);

	object_class->finalize = lu_device_finalize;
	object_class->get_property = lu_device_get_property;
	object_class->set_property = lu_device_set_property;
	klass_device->to_string = lu_device_to_string;

	pspec = g_param_spec_uint ("kind", NULL, NULL,
				   LU_DEVICE_KIND_UNKNOWN,
				   LU_DEVICE_KIND_LAST,
				   LU_DEVICE_KIND_UNKNOWN,
				   G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
	g_object_class_install_property (object_class, PROP_KIND, pspec);

	pspec = g_param_spec_uint ("hidpp-id", NULL, NULL,
				   HIDPP_DEVICE_ID_WIRED,
				   HIDPP_DEVICE_ID_RECEIVER,
				   HIDPP_DEVICE_ID_UNSET,
				   G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
	g_object_class_install_property (object_class, PROP_HIDPP_ID, pspec);

	pspec = g_param_spec_uint64 ("flags", NULL, NULL,
				     LU_DEVICE_FLAG_NONE,
				     0xffff,
				     LU_DEVICE_FLAG_NONE,
				     G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
	g_object_class_install_property (object_class, PROP_FLAGS, pspec);

	pspec = g_param_spec_object ("udev-device", NULL, NULL,
				     G_UDEV_TYPE_DEVICE,
				     G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
	g_object_class_install_property (object_class, PROP_UDEV_DEVICE, pspec);

	pspec = g_param_spec_object ("usb-device", NULL, NULL,
				     G_USB_TYPE_DEVICE,
				     G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
	g_object_class_install_property (object_class, PROP_USB_DEVICE, pspec);
}

LuDevice *
lu_device_fake_new (LuDeviceKind kind)
{
	LuDevice *device = NULL;
	switch (kind) {
	case LU_DEVICE_KIND_BOOTLOADER_NORDIC:
		device = g_object_new (LU_TYPE_DEVICE_BOOTLOADER_NORDIC,
				       "kind", kind,
				       NULL);
		break;
	case LU_DEVICE_KIND_BOOTLOADER_TEXAS:
		device = g_object_new (LU_TYPE_DEVICE_BOOTLOADER_TEXAS,
				       "kind", kind,
				       NULL);
		break;
	case LU_DEVICE_KIND_RUNTIME:
		device = g_object_new (LU_TYPE_DEVICE_RUNTIME,
				       "kind", kind,
				       NULL);
		break;
	default:
		break;
	}
	return device;
}
