/*
 * Copyright (C) 2017 VIA Corporation
 * Copyright (C) 2019 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

#include "config.h"

#include <string.h>

#include "fu-vli-pd-common.h"
#include "fu-vli-pd-firmware.h"

#include "fu-vli-usbhub-common.h"
#include "fu-vli-usbhub-device.h"
#include "fu-vli-usbhub-pd-device.h"

struct _FuVliUsbhubPdDevice
{
	FuDevice		 parent_instance;
	FuVliDeviceKind		 device_kind;
};

G_DEFINE_TYPE (FuVliUsbhubPdDevice, fu_vli_usbhub_pd_device, FU_TYPE_DEVICE)

static void
fu_vli_usbhub_pd_device_to_string (FuDevice *device, guint idt, GString *str)
{
	FuVliUsbhubPdDevice *self = FU_VLI_USBHUB_PD_DEVICE (device);
	fu_common_string_append_kv (str, idt, "DeviceKind",
				    fu_vli_common_device_kind_to_string (self->device_kind));
	fu_common_string_append_kx (str, idt, "FwOffset",
				    fu_vli_common_device_kind_get_offset (self->device_kind));
	fu_common_string_append_kx (str, idt, "FwSize",
				    fu_vli_common_device_kind_get_size (self->device_kind));
}

static gboolean
fu_vli_usbhub_pd_device_setup (FuDevice *device, GError **error)
{
	FuVliPdHdr hdr = { 0x0 };
	FuVliUsbhubPdDevice *self = FU_VLI_USBHUB_PD_DEVICE (device);
	FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE (fu_device_get_parent (device));
	guint32 fwver;
	g_autofree gchar *fwver_str = NULL;
	g_autofree gchar *instance_id0 = NULL;
	g_autofree gchar *instance_id1 = NULL;
	g_autofree gchar *instance_id2 = NULL;
	g_autofree gchar *instance_id3 = NULL;

	/* legacy location */
	if (!fu_vli_device_spi_read_block (FU_VLI_DEVICE (parent),
					   VLI_USBHUB_FLASHMAP_ADDR_PD_LEGACY +
					   VLI_USBHUB_PD_FLASHMAP_ADDR_LEGACY,
					   (guint8 *) &hdr, sizeof(hdr), error)) {
		g_prefix_error (error, "failed to read legacy PD header: ");
		return FALSE;
	}

	/* new location */
	if (GUINT16_FROM_LE (hdr.vid) != 0x2109) {
		g_debug ("PD VID was 0x%04x trying new location",
			 GUINT16_FROM_LE (hdr.vid));
		if (!fu_vli_device_spi_read_block (FU_VLI_DEVICE (parent),
						   VLI_USBHUB_FLASHMAP_ADDR_PD +
						   VLI_USBHUB_PD_FLASHMAP_ADDR,
						   (guint8 *) &hdr, sizeof(hdr), error)) {
			g_prefix_error (error, "failed to read PD header: ");
			return FALSE;
		}
	}

	/* just empty space */
	if (hdr.fwver == G_MAXUINT32) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_FOUND,
			     "no PD device header found");
		return FALSE;
	}

	/* get version */
	fwver = GUINT32_FROM_BE (hdr.fwver);
	self->device_kind = fu_vli_pd_common_guess_device_kind (fwver);
	if (self->device_kind == FU_VLI_DEVICE_KIND_UNKNOWN) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "PD version invalid [0x%x]", fwver);
		return FALSE;
	}
	fu_device_set_name (device, fu_vli_common_device_kind_to_string (self->device_kind));

	/* use header to populate device info */
	fu_device_set_version_raw (device, fwver);
	fwver_str = fu_common_version_from_uint32 (fwver, FWUPD_VERSION_FORMAT_QUAD);
	fu_device_set_version (device, fwver_str);
	instance_id0 = g_strdup_printf ("USB\\VID_%04X&PID_%04X&APP_%02X",
					GUINT16_FROM_LE (hdr.vid),
					GUINT16_FROM_LE (hdr.pid),
					fwver & 0xff);
	fu_device_add_instance_id (device, instance_id0);
	instance_id1 = g_strdup_printf ("USB\\VID_%04X&PID_%04X&DEV_%s",
					GUINT16_FROM_LE (hdr.vid),
					GUINT16_FROM_LE (hdr.pid),
					fu_vli_common_device_kind_to_string (self->device_kind));
	fu_device_add_instance_id (device, instance_id1);

	/* add standard GUIDs in order of priority */
	instance_id2 = g_strdup_printf ("USB\\VID_%04X&PID_%04X",
					GUINT16_FROM_LE (hdr.vid),
					GUINT16_FROM_LE (hdr.pid));
	fu_device_add_instance_id (device, instance_id2);
	instance_id3 = g_strdup_printf ("USB\\VID_%04X",
					GUINT16_FROM_LE (hdr.vid));
	fu_device_add_instance_id_full (device, instance_id3,
					FU_DEVICE_INSTANCE_FLAG_ONLY_QUIRKS);

	/* these have a backup section */
	if (fu_vli_common_device_kind_get_offset (self->device_kind) == VLI_USBHUB_FLASHMAP_ADDR_PD)
		fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_SELF_RECOVERY);

	/* success */
	return TRUE;
}

static gboolean
fu_vli_usbhub_pd_device_reload (FuDevice *device, GError **error)
{
	FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE (fu_device_get_parent (device));
	g_autoptr(FuDeviceLocker) locker = NULL;

	/* open parent device */
	locker = fu_device_locker_new (parent, error);
	if (locker == NULL)
		return FALSE;
	return fu_vli_usbhub_pd_device_setup (device, error);
}

static FuFirmware *
fu_vli_usbhub_pd_device_prepare_firmware (FuDevice *device,
				       GBytes *fw,
				       FwupdInstallFlags flags,
				       GError **error)
{
	FuVliUsbhubPdDevice *self = FU_VLI_USBHUB_PD_DEVICE (device);
	FuVliDeviceKind device_kind;
	g_autoptr(FuFirmware) firmware = fu_vli_pd_firmware_new ();

	/* check is compatible with firmware */
	if (!fu_firmware_parse (firmware, fw, flags, error))
		return NULL;
	device_kind = fu_vli_pd_firmware_get_kind (FU_VLI_PD_FIRMWARE (firmware));
	if (self->device_kind != device_kind) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INVALID_FILE,
			     "firmware incompatible, got %s, expected %s",
			     fu_vli_common_device_kind_to_string (device_kind),
			     fu_vli_common_device_kind_to_string (self->device_kind));
		return NULL;
	}

	/* we could check this against flags */
	g_debug ("parsed version: %s", fu_firmware_get_version (firmware));
	return g_steal_pointer (&firmware);
}

static GBytes *
fu_vli_usbhub_pd_device_dump_firmware (FuDevice *device, GError **error)
{
	FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE (fu_device_get_parent (device));
	FuVliUsbhubPdDevice *self = FU_VLI_USBHUB_PD_DEVICE (device);
	g_autoptr(FuDeviceLocker) locker = NULL;

	/* open device */
	locker = fu_device_locker_new (parent, error);
	if (locker == NULL)
		return NULL;

	/* read */
	fu_device_set_status (FU_DEVICE (device), FWUPD_STATUS_DEVICE_READ);
	return fu_vli_device_spi_read (FU_VLI_DEVICE (parent),
				       fu_vli_common_device_kind_get_offset (self->device_kind),
				       fu_device_get_firmware_size_max (device),
				       error);
}

static gboolean
fu_vli_usbhub_pd_device_write_firmware (FuDevice *device,
					FuFirmware *firmware,
					FwupdInstallFlags flags,
					GError **error)
{
	FuVliUsbhubPdDevice *self = FU_VLI_USBHUB_PD_DEVICE (device);
	FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE (fu_device_get_parent (device));
	gsize bufsz = 0;
	const guint8 *buf;
	g_autoptr(FuDeviceLocker) locker = NULL;
	g_autoptr(GBytes) fw = NULL;

	/* simple image */
	fw = fu_firmware_get_image_default_bytes (firmware, error);
	if (fw == NULL)
		return FALSE;

	/* open device */
	locker = fu_device_locker_new (parent, error);
	if (locker == NULL)
		return FALSE;

	/* erase */
	fu_device_set_status (device, FWUPD_STATUS_DEVICE_ERASE);
	buf = g_bytes_get_data (fw, &bufsz);
	if (!fu_vli_device_spi_erase (FU_VLI_DEVICE (parent),
				      fu_vli_common_device_kind_get_offset (self->device_kind),
				      bufsz, error))
		return FALSE;

	/* write */
	fu_device_set_status (device, FWUPD_STATUS_DEVICE_WRITE);
	if (!fu_vli_device_spi_write (FU_VLI_DEVICE (parent),
				      fu_vli_common_device_kind_get_offset (self->device_kind),
				      buf, bufsz, error))
		return FALSE;

	/* success */
	return TRUE;
}

/* reboot the parent FuVliUsbhubDevice if we update the FuVliUsbhubPdDevice */
static gboolean
fu_vli_usbhub_pd_device_attach (FuDevice *device, GError **error)
{
	FuDevice *parent = fu_device_get_parent (device);
	g_autoptr(FuDeviceLocker) locker = fu_device_locker_new (parent, error);
	if (locker == NULL)
		return FALSE;
	return fu_device_attach (parent, error);
}

static void
fu_vli_usbhub_pd_device_init (FuVliUsbhubPdDevice *self)
{
	fu_device_add_icon (FU_DEVICE (self), "audio-card");
	fu_device_set_protocol (FU_DEVICE (self), "com.vli.usbhub");
	fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_UPDATABLE);
	fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE);
	fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_NO_GUID_MATCHING);
	fu_device_set_version_format (FU_DEVICE (self), FWUPD_VERSION_FORMAT_QUAD);
	fu_device_set_install_duration (FU_DEVICE (self), 15); /* seconds */
	fu_device_set_logical_id (FU_DEVICE (self), "PD");
	fu_device_set_summary (FU_DEVICE (self), "USB-C Power Delivery Device");
}

static void
fu_vli_usbhub_pd_device_class_init (FuVliUsbhubPdDeviceClass *klass)
{
	FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass);
	klass_device->to_string = fu_vli_usbhub_pd_device_to_string;
	klass_device->setup = fu_vli_usbhub_pd_device_setup;
	klass_device->reload = fu_vli_usbhub_pd_device_reload;
	klass_device->attach = fu_vli_usbhub_pd_device_attach;
	klass_device->dump_firmware = fu_vli_usbhub_pd_device_dump_firmware;
	klass_device->write_firmware = fu_vli_usbhub_pd_device_write_firmware;
	klass_device->prepare_firmware = fu_vli_usbhub_pd_device_prepare_firmware;
}

FuDevice *
fu_vli_usbhub_pd_device_new (FuVliUsbhubDevice *parent)
{
	FuVliUsbhubPdDevice *self = g_object_new (FU_TYPE_VLI_USBHUB_PD_DEVICE,
						  "parent", parent,
						   NULL);
	return FU_DEVICE (self);
}
