/*
 * Copyright 2018 Realtek Semiconductor Corporation
 * Copyright 2018 Dell Inc.
 * All rights reserved.
 *
 * This software and associated documentation (if any) is furnished
 * under a license and may only be used or copied in accordance
 * with the terms of the license.
 *
 * This file is provided under a dual MIT/LGPLv2 license.  When using or
 * redistributing this file, you may do so under either license.
 * Dell Chooses the MIT license part of Dual MIT/LGPLv2 license agreement.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT
 */

#include "config.h"

#include <errno.h>
#include <string.h>

#include "fu-dell-dock-hid.h"

#define HIDI2C_MAX_REGISTER	   4
#define HID_MAX_RETRIES		   5
#define TBT_MAX_RETRIES		   2
#define HIDI2C_TRANSACTION_TIMEOUT 2000

#define HUB_CMD_READ_DATA	0xC0
#define HUB_CMD_WRITE_DATA	0x40
#define HUB_EXT_READ_STATUS	0x09
#define HUB_EXT_MCUMODIFYCLOCK	0x06
#define HUB_EXT_I2C_WRITE	0xC6
#define HUB_EXT_WRITEFLASH	0xC8
#define HUB_EXT_I2C_READ	0xD6
#define HUB_EXT_VERIFYUPDATE	0xD9
#define HUB_EXT_ERASEBANK	0xE8
#define HUB_EXT_WRITE_TBT_FLASH 0xFF

#define TBT_COMMAND_WAKEUP		0x00000000
#define TBT_COMMAND_AUTHENTICATE	0xFFFFFFFF
#define TBT_COMMAND_AUTHENTICATE_STATUS 0xFFFFFFFE

typedef struct __attribute__((packed)) { /* nocheck:blocked */
	guint8 cmd;
	guint8 ext;
	union {
		guint32 dwregaddr;
		struct {
			guint8 cmd_data0;
			guint8 cmd_data1;
			guint8 cmd_data2;
			guint8 cmd_data3;
		};
	};
	guint16 bufferlen;
	FuHIDI2CParameters parameters;
	guint8 extended_cmdarea[53];
	guint8 data[192];
} FuHIDCmdBuffer;

typedef struct __attribute__((packed)) { /* nocheck:blocked */
	guint8 cmd;
	guint8 ext;
	guint8 i2ctargetaddr;
	guint8 i2cspeed;
	union {
		guint32 startaddress;
		guint32 tbt_command;
	};
	guint8 bufferlen;
	guint8 extended_cmdarea[55];
	guint8 data[192];
} FuTbtCmdBuffer;

static gboolean
fu_dell_dock_hid_set_report_cb(FuDevice *device, gpointer user_data, GError **error)
{
	guint8 *outbuffer = (guint8 *)user_data;
	return fu_hid_device_set_report(FU_HID_DEVICE(device),
					0x0,
					outbuffer,
					192,
					HIDI2C_TRANSACTION_TIMEOUT,
					FU_HID_DEVICE_FLAG_NONE,
					error);
}

/* nocheck:name -- this should probably be implemented using an interface */
static gboolean
fu_dell_dock_hid_set_report(FuDevice *device, guint8 *outbuffer, GError **error)
{
	return fu_device_retry(device,
			       fu_dell_dock_hid_set_report_cb,
			       HID_MAX_RETRIES,
			       outbuffer,
			       error);
}

static gboolean
fu_dell_dock_hid_get_report_cb(FuDevice *device, gpointer user_data, GError **error)
{
	guint8 *inbuffer = (guint8 *)user_data;
	return fu_hid_device_get_report(FU_HID_DEVICE(device),
					0x0,
					inbuffer,
					192,
					HIDI2C_TRANSACTION_TIMEOUT,
					FU_HID_DEVICE_FLAG_NONE,
					error);
}

/* nocheck:name -- this should probably be implemented using an interface */
static gboolean
fu_dell_dock_hid_get_report(FuDevice *device, guint8 *inbuffer, GError **error)
{
	return fu_device_retry(device,
			       fu_dell_dock_hid_get_report_cb,
			       HID_MAX_RETRIES,
			       inbuffer,
			       error);
}

/* nocheck:name -- this should probably be implemented using an interface */
gboolean
fu_dell_dock_hid_get_hub_version(FuDevice *device, GError **error)
{
	g_autofree gchar *version = NULL;
	FuHIDCmdBuffer cmd_buffer = {
	    .cmd = HUB_CMD_READ_DATA,
	    .ext = HUB_EXT_READ_STATUS,
	    .cmd_data0 = 0,
	    .cmd_data1 = 0,
	    .cmd_data2 = 0,
	    .cmd_data3 = 0,
	    .bufferlen = GUINT16_TO_LE(12), /* nocheck:blocked */
	    .parameters = {.i2ctargetaddr = 0, .regaddrlen = 0, .i2cspeed = 0},
	    .extended_cmdarea[0 ... 52] = 0,
	};

	if (!fu_dell_dock_hid_set_report(device, (guint8 *)&cmd_buffer, error)) {
		g_prefix_error_literal(error, "failed to query hub version: ");
		return FALSE;
	}
	if (!fu_dell_dock_hid_get_report(device, cmd_buffer.data, error)) {
		g_prefix_error_literal(error, "failed to query hub version: ");
		return FALSE;
	}

	version = g_strdup_printf("%02x.%02x", cmd_buffer.data[10], cmd_buffer.data[11]);
	fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_PAIR);
	fu_device_set_version(device, version);
	return TRUE;
}

/* nocheck:name -- this should probably be implemented using an interface */
gboolean
fu_dell_dock_hid_raise_mcu_clock(FuDevice *device, gboolean enable, GError **error)
{
	FuHIDCmdBuffer cmd_buffer = {
	    .cmd = HUB_CMD_WRITE_DATA,
	    .ext = HUB_EXT_MCUMODIFYCLOCK,
	    .cmd_data0 = (guint8)enable,
	    .cmd_data1 = 0,
	    .cmd_data2 = 0,
	    .cmd_data3 = 0,
	    .bufferlen = 0,
	    .parameters = {.i2ctargetaddr = 0, .regaddrlen = 0, .i2cspeed = 0},
	    .extended_cmdarea[0 ... 52] = 0,
	};

	if (!fu_dell_dock_hid_set_report(device, (guint8 *)&cmd_buffer, error)) {
		g_prefix_error(error, "failed to set mcu clock to %d: ", enable);
		return FALSE;
	}

	return TRUE;
}

/* nocheck:name -- this should probably be implemented using an interface */
gboolean
fu_dell_dock_hid_erase_bank(FuDevice *device, guint8 idx, GError **error)
{
	FuHIDCmdBuffer cmd_buffer = {
	    .cmd = HUB_CMD_WRITE_DATA,
	    .ext = HUB_EXT_ERASEBANK,
	    .cmd_data0 = 0,
	    .cmd_data1 = idx,
	    .cmd_data2 = 0,
	    .cmd_data3 = 0,
	    .bufferlen = 0,
	    .parameters = {.i2ctargetaddr = 0, .regaddrlen = 0, .i2cspeed = 0},
	    .extended_cmdarea[0 ... 52] = 0,
	};

	if (!fu_dell_dock_hid_set_report(device, (guint8 *)&cmd_buffer, error)) {
		g_prefix_error_literal(error, "failed to erase bank: ");
		return FALSE;
	}

	return TRUE;
}

/* nocheck:name -- this should probably be implemented using an interface */
gboolean
fu_dell_dock_hid_write_flash(FuDevice *device,
			     guint32 dwAddr,
			     const guint8 *input,
			     gsize write_size,
			     GError **error)
{
	FuHIDCmdBuffer cmd_buffer = {
	    .cmd = HUB_CMD_WRITE_DATA,
	    .ext = HUB_EXT_WRITEFLASH,
	    .dwregaddr = GUINT32_TO_LE(dwAddr),	    /* nocheck:blocked */
	    .bufferlen = GUINT16_TO_LE(write_size), /* nocheck:blocked */
	    .parameters = {.i2ctargetaddr = 0, .regaddrlen = 0, .i2cspeed = 0},
	    .extended_cmdarea[0 ... 52] = 0,
	};

	g_return_val_if_fail(write_size <= HIDI2C_MAX_WRITE, FALSE);

	memcpy(cmd_buffer.data, input, write_size); /* nocheck:blocked */
	if (!fu_dell_dock_hid_set_report(device, (guint8 *)&cmd_buffer, error)) {
		g_prefix_error(error, "failed to write %zu flash to %x: ", write_size, dwAddr);
		return FALSE;
	}

	return TRUE;
}

/* nocheck:name -- this should probably be implemented using an interface */
gboolean
fu_dell_dock_hid_verify_update(FuDevice *device, gboolean *result, GError **error)
{
	FuHIDCmdBuffer cmd_buffer = {
	    .cmd = HUB_CMD_WRITE_DATA,
	    .ext = HUB_EXT_VERIFYUPDATE,
	    .cmd_data0 = 1,
	    .cmd_data1 = 0,
	    .cmd_data2 = 0,
	    .cmd_data3 = 0,
	    .bufferlen = GUINT16_TO_LE(1), /* nocheck:blocked */
	    .parameters = {.i2ctargetaddr = 0, .regaddrlen = 0, .i2cspeed = 0},
	    .extended_cmdarea[0 ... 52] = 0,
	};

	if (!fu_dell_dock_hid_set_report(device, (guint8 *)&cmd_buffer, error)) {
		g_prefix_error_literal(error, "failed to verify update: ");
		return FALSE;
	}
	if (!fu_dell_dock_hid_get_report(device, cmd_buffer.data, error)) {
		g_prefix_error_literal(error, "failed to verify update: ");
		return FALSE;
	}
	*result = cmd_buffer.data[0];

	return TRUE;
}

/* nocheck:name -- this should probably be implemented using an interface */
gboolean
fu_dell_dock_hid_i2c_write(FuDevice *device,
			   const guint8 *input,
			   gsize write_size,
			   const FuHIDI2CParameters *parameters,
			   GError **error)
{
	FuHIDCmdBuffer cmd_buffer = {
	    .cmd = HUB_CMD_WRITE_DATA,
	    .ext = HUB_EXT_I2C_WRITE,
	    .dwregaddr = 0,
	    .bufferlen = GUINT16_TO_LE(write_size), /* nocheck:blocked */
	    .parameters = {.i2ctargetaddr = parameters->i2ctargetaddr,
			   .regaddrlen = 0,
			   .i2cspeed = parameters->i2cspeed | 0x80},
	    .extended_cmdarea[0 ... 52] = 0,
	};

	g_return_val_if_fail(write_size <= HIDI2C_MAX_WRITE, FALSE);

	memcpy(cmd_buffer.data, input, write_size); /* nocheck:blocked */

	return fu_dell_dock_hid_set_report(device, (guint8 *)&cmd_buffer, error);
}

/* nocheck:name -- this should probably be implemented using an interface */
gboolean
fu_dell_dock_hid_i2c_read(FuDevice *device,
			  guint32 cmd,
			  gsize read_size,
			  GBytes **bytes,
			  const FuHIDI2CParameters *parameters,
			  GError **error)
{
	FuHIDCmdBuffer cmd_buffer = {
	    .cmd = HUB_CMD_WRITE_DATA,
	    .ext = HUB_EXT_I2C_READ,
	    .dwregaddr = GUINT32_TO_LE(cmd),	   /* nocheck:blocked */
	    .bufferlen = GUINT16_TO_LE(read_size), /* nocheck:blocked */
	    .parameters = {.i2ctargetaddr = parameters->i2ctargetaddr,
			   .regaddrlen = parameters->regaddrlen,
			   .i2cspeed = parameters->i2cspeed | 0x80},
	    .extended_cmdarea[0 ... 52] = 0,
	    .data[0 ... 191] = 0,
	};

	g_return_val_if_fail(read_size <= HIDI2C_MAX_READ, FALSE);
	g_return_val_if_fail(bytes != NULL, FALSE);
	g_return_val_if_fail(parameters->regaddrlen < HIDI2C_MAX_REGISTER, FALSE);

	if (!fu_dell_dock_hid_set_report(device, (guint8 *)&cmd_buffer, error))
		return FALSE;
	if (!fu_dell_dock_hid_get_report(device, cmd_buffer.data, error))
		return FALSE;

	*bytes = g_bytes_new(cmd_buffer.data, read_size);

	return TRUE;
}

/* nocheck:name -- this should probably be implemented using an interface */
gboolean
fu_dell_dock_hid_tbt_wake(FuDevice *device, const FuHIDI2CParameters *parameters, GError **error)
{
	FuTbtCmdBuffer cmd_buffer = {
	    .cmd = HUB_CMD_READ_DATA, /* special write command that reads status result */
	    .ext = HUB_EXT_WRITE_TBT_FLASH,
	    .i2ctargetaddr = parameters->i2ctargetaddr,
	    .i2cspeed = parameters->i2cspeed, /* unlike other commands doesn't need | 0x80 */
	    .tbt_command = TBT_COMMAND_WAKEUP,
	    .bufferlen = 0,
	    .extended_cmdarea[0 ... 53] = 0,
	    .data[0 ... 191] = 0,
	};

	if (!fu_dell_dock_hid_set_report(device, (guint8 *)&cmd_buffer, error)) {
		g_prefix_error_literal(error, "failed to set wake thunderbolt: ");
		return FALSE;
	}
	if (!fu_dell_dock_hid_get_report(device, cmd_buffer.data, error)) {
		g_prefix_error_literal(error, "failed to get wake thunderbolt status: ");
		return FALSE;
	}
	g_debug("thunderbolt wake result: 0x%x", cmd_buffer.data[1]);

	return TRUE;
}

static const gchar *
fu_dell_dock_hid_tbt_map_error(guint32 code)
{
	if (code == 1)
		return fwupd_strerror(EINVAL);
	if (code == 2)
		return fwupd_strerror(EPERM);

	return fwupd_strerror(EIO);
}

/* nocheck:name -- this should probably be implemented using an interface */
gboolean
fu_dell_dock_hid_tbt_write(FuDevice *device,
			   guint32 start_addr,
			   const guint8 *input,
			   gsize write_size,
			   const FuHIDI2CParameters *parameters,
			   GError **error)
{
	FuTbtCmdBuffer cmd_buffer = {
	    .cmd = HUB_CMD_READ_DATA, /* a special write command that reads status result */
	    .ext = HUB_EXT_WRITE_TBT_FLASH,
	    .i2ctargetaddr = parameters->i2ctargetaddr,
	    .i2cspeed = parameters->i2cspeed, /* unlike other commands doesn't need | 0x80 */
	    .startaddress = GUINT32_TO_LE(start_addr), /* nocheck:blocked */
	    .bufferlen = write_size,
	    .extended_cmdarea[0 ... 53] = 0,
	};
	guint8 result;

	g_return_val_if_fail(input != NULL, FALSE);
	g_return_val_if_fail(write_size <= HIDI2C_MAX_WRITE, FALSE);

	memcpy(cmd_buffer.data, input, write_size); /* nocheck:blocked */

	for (gint i = 1; i <= TBT_MAX_RETRIES; i++) {
		if (!fu_dell_dock_hid_set_report(device, (guint8 *)&cmd_buffer, error)) {
			g_prefix_error_literal(error, "failed to run TBT update: ");
			return FALSE;
		}
		if (!fu_dell_dock_hid_get_report(device, cmd_buffer.data, error)) {
			g_prefix_error_literal(error, "failed to get TBT flash status: ");
			return FALSE;
		}
		result = cmd_buffer.data[1] & 0xf;
		if (result == 0)
			break;
		g_debug("attempt %d/%d: Thunderbolt write failed: %x", i, TBT_MAX_RETRIES, result);
	}
	if (result != 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "Writing address 0x%04x failed: %s",
			    start_addr,
			    fu_dell_dock_hid_tbt_map_error(result));
		return FALSE;
	}

	return TRUE;
}

/* nocheck:name -- this should probably be implemented using an interface */
gboolean
fu_dell_dock_hid_tbt_authenticate(FuDevice *device,
				  const FuHIDI2CParameters *parameters,
				  GError **error)
{
	FuTbtCmdBuffer cmd_buffer = {
	    .cmd = HUB_CMD_READ_DATA, /* a special write command that reads status result */
	    .ext = HUB_EXT_WRITE_TBT_FLASH,
	    .i2ctargetaddr = parameters->i2ctargetaddr,
	    .i2cspeed = parameters->i2cspeed, /* unlike other commands doesn't need | 0x80 */
	    .tbt_command = GUINT32_TO_LE(TBT_COMMAND_AUTHENTICATE), /* nocheck:blocked */
	    .bufferlen = 0,
	    .extended_cmdarea[0 ... 53] = 0,
	};
	guint8 result;

	if (!fu_dell_dock_hid_set_report(device, (guint8 *)&cmd_buffer, error)) {
		g_prefix_error_literal(error, "failed to send authentication: ");
		return FALSE;
	}

	cmd_buffer.tbt_command =
	    GUINT32_TO_LE(TBT_COMMAND_AUTHENTICATE_STATUS); /* nocheck:blocked */
	/* needs at least 2 seconds */
	fu_device_sleep(device, 2000);
	for (gint i = 1; i <= TBT_MAX_RETRIES; i++) {
		if (!fu_dell_dock_hid_set_report(device, (guint8 *)&cmd_buffer, error)) {
			g_prefix_error_literal(error, "failed to set check authentication: ");
			return FALSE;
		}
		if (!fu_dell_dock_hid_get_report(device, cmd_buffer.data, error)) {
			g_prefix_error_literal(error, "failed to get check authentication: ");
			return FALSE;
		}
		result = cmd_buffer.data[1] & 0xf;
		if (result == 0)
			break;
		g_debug("attempt %d/%d: Thunderbolt authenticate failed: %x",
			i,
			TBT_MAX_RETRIES,
			result);
		fu_device_sleep(device, 500); /* ms */
	}
	if (result != 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_AUTH_FAILED,
			    "thunderbolt authentication failed: %s",
			    fu_dell_dock_hid_tbt_map_error(result));
		return FALSE;
	}

	return TRUE;
}
