/*
 * Copyright (C) 2016 Richard Hughes <richard@hughsie.com>
 * Copyright (C) 2016 Mario Limonciello <mario.limonciello@dell.com>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

#include "config.h"

#include <glib/gstdio.h>
#include <smbios_c/system_info.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

#include "fwupd-common.h"
#include "fu-plugin-dell.h"
#include "fu-plugin-vfuncs.h"
#include "fu-device-metadata.h"

/* These are used to indicate the status of a previous DELL flash */
#define DELL_SUCCESS			0x0000
#define DELL_CONSISTENCY_FAIL		0x0001
#define DELL_FLASH_MEMORY_FAIL		0x0002
#define DELL_FLASH_NOT_READY		0x0003
#define DELL_FLASH_DISABLED		0x0004
#define DELL_BATTERY_MISSING		0x0005
#define DELL_BATTERY_DEAD		0x0006
#define DELL_AC_MISSING			0x0007
#define DELL_CANT_SET_12V		0x0008
#define DELL_CANT_UNSET_12V		0x0009
#define DELL_FAILURE_BLOCK_ERASE	0x000A
#define DELL_GENERAL_FAILURE		0x000B
#define DELL_DATA_MISCOMPARE		0x000C
#define DELL_IMAGE_MISSING		0x000D
#define DELL_DID_NOTHING		0xFFFF

/* Delay for settling */
#define DELL_FLASH_MODE_DELAY		2

typedef struct _DOCK_DESCRIPTION
{
	const gchar *		guid;
	const gchar *		query;
	const gchar *		desc;
} DOCK_DESCRIPTION;

struct da_structure {
	guint8			 type;
	guint8			 length;
	guint16			 handle;
	guint16			 cmd_address;
	guint8			 cmd_code;
	guint32			 supported_cmds;
	guint8			*tokens;
} __attribute__((packed));

/* These are for matching the components */
#define WD15_EC_STR		"2 0 2 2 0"
#define TB16_EC_STR		"2 0 2 1 0"
#define TB16_PC2_STR		"2 1 0 1 1"
#define TB16_PC1_STR		"2 1 0 1 0"
#define WD15_PC1_STR		"2 1 0 2 0"
#define LEGACY_CBL_STR		"2 2 2 1 0"
#define UNIV_CBL_STR		"2 2 2 2 0"
#define TBT_CBL_STR		"2 2 2 3 0"
#define FUTURE_EC_STR		"3 0 2 4 0"
#define FUTURE_EC_STR2		"4 0 2 4 0"

/* supported dock related GUIDs */
#define DOCK_FLASH_GUID		"e7ca1f36-bf73-4574-afe6-a4ccacabf479"
#define WD15_EC_GUID		"e8445370-0211-449d-9faa-107906ab189f"
#define TB16_EC_GUID		"33cc8870-b1fc-4ec7-948a-c07496874faf"
#define TB16_PC2_GUID		"1b52c630-86f6-4aee-9f0c-474dc6be49b6"
#define TB16_PC1_GUID		"8fe183da-c94e-4804-b319-0f1ba5457a69"
#define WD15_PC1_GUID		"8ba2b709-6f97-47fc-b7e7-6a87b578fe25"
#define LEGACY_CBL_GUID		"fece1537-d683-4ea8-b968-154530bb6f73"
#define UNIV_CBL_GUID		"e2bf3aad-61a3-44bf-91ef-349b39515d29"
#define TBT_CBL_GUID		"6dc832fc-5bb0-4e63-a2ff-02aaba5bc1dc"

#define EC_DESC			"EC"
#define PC1_DESC		"Port Controller 1"
#define PC2_DESC		"Port Controller 2"
#define LEGACY_CBL_DESC		"Passive Cable"
#define UNIV_CBL_DESC		"Universal Cable"
#define TBT_CBL_DESC		"Thunderbolt Cable"

/* supported host related GUIDs */
#define MST_GPIO_GUID		EFI_GUID (0xF24F9bE4, 0x2a13, 0x4344, 0xBC05, 0x01, 0xCE, 0xF7, 0xDA, 0xEF, 0x92)

/**
 * Devices that should allow modeswitching
 */
static guint16 tpm_switch_whitelist[] = {0x06F2, 0x06F3, 0x06DD, 0x06DE, 0x06DF,
					 0x06DB, 0x06DC, 0x06BB, 0x06C6, 0x06BA,
					 0x06B9, 0x05CA, 0x06C7, 0x06B7, 0x06E0,
					 0x06E5, 0x06D9, 0x06DA, 0x06E4, 0x0704,
					 0x0720, 0x0730, 0x0758, 0x0759, 0x075B,
					 0x07A0, 0x079F, 0x07A4, 0x07A5, 0x07A6,
					 0x07A7, 0x07A8, 0x07A9, 0x07AA, 0x07AB,
					 0x07B0, 0x07B1, 0x07B2, 0x07B4, 0x07B7,
					 0x07B8, 0x07B9, 0x07BE, 0x07BF, 0x077A,
					 0x07CF};
/**
  * Dell device types to run
  */
static guint8 enclosure_whitelist [] = { 0x03, /* desktop */
					 0x04, /* low profile desktop */
					 0x06, /* mini tower */
					 0x07, /* tower */
					 0x08, /* portable */
					 0x09, /* laptop */
					 0x0A, /* notebook */
					 0x0D, /* AIO */
					 0x1E, /* tablet */
					 0x1F, /* convertible */
					 0x21, /* IoT gateway */
					 0x22, /* embedded PC */};

/**
  * Systems containing host MST device
  */
static guint16 systems_host_mst [] =	{ 0x062d, /* Latitude E7250 */
					  0x062e, /* Latitude E7450 */
					  0x062a, /* Latitude E5250 */
					  0x062b, /* Latitude E5450 */
					  0x062c, /* Latitude E5550 */
					  0x06db, /* Latitude E7270 */
					  0x06dc, /* Latitude E7470 */
					  0x06dd, /* Latitude E5270 */
					  0x06de, /* Latitude E5470 */
					  0x06df, /* Latitude E5570 */
					  0x06e0, /* Precision 3510 */
					  0x071d, /* Latitude Rugged 7214 */
					  0x071e, /* Latitude Rugged 5414 */
					  0x071c, /* Latitude Rugged 7414 */};

static guint16
fu_dell_get_system_id (FuPlugin *plugin)
{
	const gchar *system_id_str = NULL;
	guint16 system_id = 0;
	gchar *endptr = NULL;

	system_id_str = fu_plugin_get_dmi_value (plugin,
		FU_HWIDS_KEY_PRODUCT_SKU);
	if (system_id_str != NULL)
		system_id = g_ascii_strtoull (system_id_str, &endptr, 16);
	if (system_id == 0 || endptr == system_id_str)
		system_id = (guint16) sysinfo_get_dell_system_id ();

	return system_id;
}

static gboolean
fu_dell_host_mst_supported (FuPlugin *plugin)
{
	guint16 system_id;

	system_id = fu_dell_get_system_id (plugin);
	if (system_id == 0)
		return FALSE;
	for (guint i = 0; i < G_N_ELEMENTS (systems_host_mst); i++)
		if (systems_host_mst[i] == system_id)
			return TRUE;
	return FALSE;
}

static gboolean
fu_dell_supported (FuPlugin *plugin)
{
	g_autoptr(GBytes) de_table = NULL;
	g_autoptr(GBytes) da_table = NULL;
	g_autoptr(GBytes) enclosure = NULL;
	const guint8 *value;
	const struct da_structure *da_values;
	gsize len;

	/* make sure that Dell SMBIOS methods are available */
	de_table = fu_plugin_get_smbios_data (plugin, 0xDE);
	if (de_table == NULL)
		return FALSE;
	value = g_bytes_get_data (de_table, &len);
	if (len == 0)
		return FALSE;
	if (*value != 0xDE)
		return FALSE;
	da_table = fu_plugin_get_smbios_data (plugin, 0xDA);
	if (da_table == NULL)
		return FALSE;
	da_values = (struct da_structure *) g_bytes_get_data (da_table, &len);
	if (len == 0)
		return FALSE;
	if (!(da_values->supported_cmds & (1 << DACI_FLASH_INTERFACE_CLASS))) {
		g_debug ("unable to access flash interface. supported commands: 0x%x",
			 da_values->supported_cmds);
		return FALSE;
	}

	/* only run on intended Dell hw types */
	enclosure = fu_plugin_get_smbios_data (plugin,
					       FU_SMBIOS_STRUCTURE_TYPE_CHASSIS);
	if (enclosure == NULL)
		return FALSE;
	value = g_bytes_get_data (enclosure, &len);
	if (len == 0)
		return FALSE;
	for (guint i = 0; i < G_N_ELEMENTS (enclosure_whitelist); i++) {
		if (enclosure_whitelist[i] == value[0])
			return TRUE;
	}

	return FALSE;
}

static gboolean
fu_plugin_dell_match_dock_component (const gchar *query_str,
				     const gchar **guid_out,
				     const gchar **name_out)
{
	const DOCK_DESCRIPTION list[] = {
		{WD15_EC_GUID, WD15_EC_STR, EC_DESC},
		{TB16_EC_GUID, TB16_EC_STR, EC_DESC},
		{WD15_PC1_GUID, WD15_PC1_STR, PC1_DESC},
		{TB16_PC1_GUID, TB16_PC1_STR, PC1_DESC},
		{TB16_PC2_GUID, TB16_PC2_STR, PC2_DESC},
		{TBT_CBL_GUID, TBT_CBL_STR, TBT_CBL_DESC},
		{UNIV_CBL_GUID, UNIV_CBL_STR, UNIV_CBL_DESC},
		{LEGACY_CBL_GUID, LEGACY_CBL_STR, LEGACY_CBL_DESC},
		{NULL, FUTURE_EC_STR, NULL},
		{NULL, FUTURE_EC_STR2, NULL},
	};

	for (guint i = 0; i < G_N_ELEMENTS (list); i++) {
		if (g_strcmp0 (query_str,
			       list[i].query) == 0) {
			*guid_out = list[i].guid;
			*name_out = list[i].desc;
			return TRUE;
		}
	}
	return FALSE;
}

void
fu_plugin_dell_inject_fake_data (FuPlugin *plugin,
				 guint32 *output, guint16 vid, guint16 pid,
				 guint8 *buf, gboolean can_switch_modes)
{
	FuPluginData *data = fu_plugin_get_data (plugin);

	if (!data->smi_obj->fake_smbios)
		return;
	for (guint i = 0; i < 4; i++)
		data->smi_obj->output[i] = output[i];
	data->fake_vid = vid;
	data->fake_pid = pid;
	data->smi_obj->fake_buffer = buf;
	data->can_switch_modes = TRUE;
}

static FwupdVersionFormat
fu_plugin_dell_get_version_format (FuPlugin *plugin)
{
	const gchar *content;
	const gchar *quirk;
	g_autofree gchar *group = NULL;

	content = fu_plugin_get_dmi_value (plugin, FU_HWIDS_KEY_MANUFACTURER);
	if (content == NULL)
		return FWUPD_VERSION_FORMAT_TRIPLET;

	/* any quirks match */
	group = g_strdup_printf ("SmbiosManufacturer=%s", content);
	quirk = fu_plugin_lookup_quirk_by_id (plugin, group,
					      FU_QUIRKS_UEFI_VERSION_FORMAT);
	if (quirk == NULL)
		return FWUPD_VERSION_FORMAT_TRIPLET;
	return fwupd_version_format_from_string (quirk);
}

static gboolean
fu_plugin_dell_capsule_supported (FuPlugin *plugin)
{
	FuPluginData *data = fu_plugin_get_data (plugin);

	return data->smi_obj->fake_smbios || data->capsule_supported;
}

static gboolean
fu_plugin_dock_node (FuPlugin *plugin, const gchar *platform,
		     guint8 type, const gchar *component_guid,
		     const gchar *component_desc, const gchar *version,
		     FwupdVersionFormat version_format)
{
	const gchar *dock_type;
	g_autofree gchar *dock_name = NULL;
	g_autoptr(FuDevice) dev = NULL;

	dock_type = fu_dell_get_dock_type (type);
	if (dock_type == NULL) {
		g_debug ("Unknown dock type %d", type);
		return FALSE;
	}

	dev = fu_device_new ();
	fu_device_set_physical_id (dev, platform);
	fu_device_set_logical_id (dev, component_guid);
	if (component_desc != NULL) {
		dock_name = g_strdup_printf ("Dell %s %s", dock_type,
					     component_desc);
		fu_device_add_parent_guid (dev, DOCK_FLASH_GUID);
	} else {
		dock_name = g_strdup_printf ("Dell %s", dock_type);
	}
	fu_device_set_vendor (dev, "Dell Inc.");
	fu_device_set_name (dev, dock_name);
	fu_device_set_metadata (dev, FU_DEVICE_METADATA_UEFI_DEVICE_KIND, "device-firmware");
	if (type == DOCK_TYPE_TB16) {
		fu_device_set_summary (dev, "A Thunderbolt™ 3 docking station");
	} else if (type == DOCK_TYPE_WD15) {
		fu_device_set_summary (dev, "A USB type-C docking station");
	}
	fu_device_add_icon (dev, "computer");
	fu_device_add_guid (dev, component_guid);
	fu_device_add_flag (dev, FWUPD_DEVICE_FLAG_REQUIRE_AC);
	if (version != NULL) {
		fu_device_set_version (dev, version, version_format);
		if (fu_plugin_dell_capsule_supported (plugin)) {
			fu_device_add_flag (dev, FWUPD_DEVICE_FLAG_UPDATABLE);
			fu_device_add_flag (dev, FWUPD_DEVICE_FLAG_NEEDS_REBOOT);
		} else {
			fu_device_set_update_error (dev,
						    "UEFI capsule updates turned off in BIOS setup");
		}
	}

	fu_plugin_device_register (plugin, dev);
	return TRUE;
}

gboolean
fu_plugin_usb_device_added (FuPlugin *plugin,
			    FuUsbDevice *device,
			    GError **error)
{
	FuPluginData *data = fu_plugin_get_data (plugin);
	FwupdVersionFormat version_format;
	guint16 pid;
	guint16 vid;
	const gchar *query_str;
	const gchar *component_guid = NULL;
	const gchar *component_name = NULL;
	const gchar *platform;
	DOCK_UNION buf;
	DOCK_INFO *dock_info;
	gboolean old_ec = FALSE;
	g_autofree gchar *flash_ver_str = NULL;

	/* don't look up immediately if a dock is connected as that would
	   mean a SMI on every USB device that showed up on the system */
	if (!data->smi_obj->fake_smbios) {
		vid = fu_usb_device_get_vid (device);
		pid = fu_usb_device_get_pid (device);
		platform = fu_device_get_physical_id (FU_DEVICE (device));
	} else {
		vid = data->fake_vid;
		pid = data->fake_pid;
		platform = "fake";
	}

	/* we're going to match on the Realtek NIC in the dock */
	if (vid != DOCK_NIC_VID || pid != DOCK_NIC_PID) {
		g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED,
			     "wrong VID/PID %04x:%04x", vid, pid);
		return FALSE;
	}

	buf.buf = NULL;
	if (!fu_dell_query_dock (data->smi_obj, &buf)) {
		g_debug ("no dock detected");
		return TRUE;
	}

	if (buf.record->dock_info_header.dir_version != 1) {
		g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED,
			     "dock info header version unknown %d",
			     buf.record->dock_info_header.dir_version);
		return FALSE;
	}

	dock_info = &buf.record->dock_info;
	g_debug ("Dock description: %s", dock_info->dock_description);
	/* Note: fw package version is deprecated, look at components instead */
	g_debug ("Dock flash pkg ver: 0x%x", dock_info->flash_pkg_version);
	if (dock_info->flash_pkg_version == 0x00ffffff)
		g_debug ("WARNING: dock flash package version invalid");
	g_debug ("Dock cable type: %" G_GUINT32_FORMAT, dock_info->cable_type);
	g_debug ("Dock location: %d", dock_info->location);
	g_debug ("Dock component count: %d", dock_info->component_count);
	version_format = fu_plugin_dell_get_version_format (plugin);

	for (guint i = 0; i < dock_info->component_count; i++) {
		g_autofree gchar *fw_str = NULL;
		if (i >= MAX_COMPONENTS) {
			g_debug ("Too many components.  Invalid: #%u", i);
			break;
		}
		g_debug ("Dock component %u: %s (version 0x%x)", i,
			 dock_info->components[i].description,
			 dock_info->components[i].fw_version);
		query_str = g_strrstr (dock_info->components[i].description,
				       "Query ");
		if (query_str == NULL) {
			g_set_error_literal (error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED,
					     "invalid dock component request");
			return FALSE;
		}
		if (!fu_plugin_dell_match_dock_component (query_str + 6,
							  &component_guid,
							  &component_name)) {
			g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED,
				     "invalid dock component request %s", query_str);
			return FALSE;
		}
		if (component_guid == NULL || component_name == NULL) {
			g_debug ("%s is supported by another plugin", query_str);
			return TRUE;
		}

		/* dock EC hasn't been updated for first time */
		if (dock_info->flash_pkg_version == 0x00ffffff) {
			old_ec = TRUE;
			dock_info->flash_pkg_version = 0;
			continue;
		}
		/* if invalid version, don't mark device for updates */
		else if (dock_info->components[i].fw_version == 0 ||
			 dock_info->components[i].fw_version == 0xffffffff) {
			old_ec = TRUE;
			continue;
		}

		fw_str = fu_common_version_from_uint32 (dock_info->components[i].fw_version,
						       version_format);
		if (!fu_plugin_dock_node (plugin,
					  platform,
					  buf.record->dock_info_header.dock_type,
					  component_guid,
					  component_name,
					  fw_str,
					  version_format)) {
			g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL,
				     "failed to create %s", component_name);
			return FALSE;
		}
	}

	/* if an old EC or invalid EC version found, create updatable parent */
	if (old_ec)
		flash_ver_str = fu_common_version_from_uint32 (dock_info->flash_pkg_version,
							       version_format);
	if (!fu_plugin_dock_node (plugin,
				  platform,
				  buf.record->dock_info_header.dock_type,
				  DOCK_FLASH_GUID,
				  NULL,
				  flash_ver_str,
				  version_format)) {
		g_set_error_literal (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL,
				    "failed to create top dock node");

		return FALSE;
	}

#if defined (HAVE_SYNAPTICS)
	fu_plugin_request_recoldplug (plugin);
#endif
	return TRUE;
}

gboolean
fu_plugin_get_results (FuPlugin *plugin, FuDevice *device, GError **error)
{
	g_autoptr(GBytes) de_table = NULL;
	const gchar *tmp = NULL;
	const guint16 *completion_code;
	gsize len;

	de_table = fu_plugin_get_smbios_data (plugin, 0xDE);
	completion_code = g_bytes_get_data (de_table, &len);
	if (len < 8) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "ERROR: Unable to read results of %s: %" G_GSIZE_FORMAT " < 8",
			     fu_device_get_name (device), len);
		return FALSE;
	}

	/* look at byte offset 0x06  for identifier meaning completion code */
	if (completion_code[3] == DELL_SUCCESS) {
		fu_device_set_update_state (device, FWUPD_UPDATE_STATE_SUCCESS);
	} else {
		FwupdUpdateState update_state = FWUPD_UPDATE_STATE_FAILED;
		switch (completion_code[3]) {
		case DELL_CONSISTENCY_FAIL:
			tmp = "The image failed one or more consistency checks.";
			break;
		case DELL_FLASH_MEMORY_FAIL:
			tmp = "The BIOS could not access the flash-memory device.";
			break;
		case DELL_FLASH_NOT_READY:
			tmp = "The flash-memory device was not ready when an erase was attempted.";
			break;
		case DELL_FLASH_DISABLED:
			tmp = "Flash programming is currently disabled on the system, or the voltage is low.";
			break;
		case DELL_BATTERY_MISSING:
			tmp = "A battery must be installed for the operation to complete.";
			update_state = FWUPD_UPDATE_STATE_FAILED_TRANSIENT;
			break;
		case DELL_BATTERY_DEAD:
			tmp = "A fully-charged battery must be present for the operation to complete.";
			update_state = FWUPD_UPDATE_STATE_FAILED_TRANSIENT;
			break;
		case DELL_AC_MISSING:
			tmp = "An external power adapter must be connected for the operation to complete.";
			update_state = FWUPD_UPDATE_STATE_FAILED_TRANSIENT;
			break;
		case DELL_CANT_SET_12V:
			tmp = "The 12V required to program the flash-memory could not be set.";
			break;
		case DELL_CANT_UNSET_12V:
			tmp = "The 12V required to program the flash-memory could not be removed.";
			break;
		case DELL_FAILURE_BLOCK_ERASE :
			tmp = "A flash-memory failure occurred during a block-erase operation.";
			break;
		case DELL_GENERAL_FAILURE:
			tmp = "A general failure occurred during the flash programming.";
			break;
		case DELL_DATA_MISCOMPARE:
			tmp = "A data miscompare error occurred during the flash programming.";
			break;
		case DELL_IMAGE_MISSING:
			tmp = "The image could not be found in memory, i.e. the header could not be located.";
			break;
		case DELL_DID_NOTHING:
			tmp = "No update operation has been performed on the system.";
			break;
		default:
			break;
		}
		fu_device_set_update_state (device, update_state);
		if (tmp != NULL)
			fu_device_set_update_error (device, tmp);
	}

	return TRUE;
}

gboolean
fu_plugin_dell_detect_tpm (FuPlugin *plugin, GError **error)
{
	FuPluginData *data = fu_plugin_get_data (plugin);
	const gchar *tpm_mode;
	const gchar *tpm_mode_alt;
	guint16 system_id = 0;
	gboolean can_switch_modes = FALSE;
	g_autofree gchar *pretty_tpm_name_alt = NULL;
	g_autofree gchar *pretty_tpm_name = NULL;
	g_autofree gchar *tpm_guid_raw_alt = NULL;
	g_autofree gchar *tpm_guid_alt = NULL;
	g_autofree gchar *tpm_guid = NULL;
	g_autofree gchar *tpm_guid_raw = NULL;
	g_autofree gchar *tpm_id_alt = NULL;
	g_autofree gchar *tpm_id = NULL;
	g_autofree gchar *version_str = NULL;
	struct tpm_status *out = NULL;
	g_autoptr (FuDevice) dev_alt = NULL;
	g_autoptr (FuDevice) dev = NULL;
	const gchar *product_name = "Unknown";

	fu_dell_clear_smi (data->smi_obj);
	out = (struct tpm_status *) data->smi_obj->output;

	/* execute TPM Status Query */
	data->smi_obj->input[0] = DACI_FLASH_ARG_TPM;
	if (!fu_dell_execute_simple_smi (data->smi_obj,
					 DACI_FLASH_INTERFACE_CLASS,
					 DACI_FLASH_INTERFACE_SELECT))
		return FALSE;

	if (out->ret != 0) {
		g_debug ("Failed to query system for TPM information: "
			 "(%" G_GUINT32_FORMAT ")", out->ret);
		return FALSE;
	}
	/* HW version is output in second /input/ arg
	 * it may be relevant as next gen TPM is enabled
	 */
	g_debug ("TPM HW version: 0x%x", data->smi_obj->input[1]);
	g_debug ("TPM Status: 0x%x", out->status);

	/* test TPM enabled (Bit 0) */
	if (!(out->status & TPM_EN_MASK)) {
		g_debug ("TPM not enabled (%x)", out->status);
		return FALSE;
	}

	/* test TPM mode to determine current mode */
	if (((out->status & TPM_TYPE_MASK) >> 8) == TPM_1_2_MODE) {
		tpm_mode = "1.2";
		tpm_mode_alt = "2.0";
	} else if (((out->status & TPM_TYPE_MASK) >> 8) == TPM_2_0_MODE) {
		tpm_mode = "2.0";
		tpm_mode_alt = "1.2";
	} else {
		g_debug ("Unable to determine TPM mode");
		return FALSE;
	}

	system_id = fu_dell_get_system_id (plugin);
	if (data->smi_obj->fake_smbios)
		can_switch_modes = data->can_switch_modes;
	else if (system_id == 0)
		return FALSE;

	for (guint i = 0; i < G_N_ELEMENTS (tpm_switch_whitelist); i++) {
		if (tpm_switch_whitelist[i] == system_id) {
			can_switch_modes = TRUE;
		}
	}

	tpm_guid_raw = g_strdup_printf ("%04x-%s", system_id, tpm_mode);
	tpm_guid = fwupd_guid_hash_string (tpm_guid_raw);
	tpm_id = g_strdup_printf ("DELL-%s" G_GUINT64_FORMAT, tpm_guid);

	tpm_guid_raw_alt = g_strdup_printf ("%04x-%s", system_id, tpm_mode_alt);
	tpm_guid_alt = fwupd_guid_hash_string (tpm_guid_raw_alt);
	tpm_id_alt = g_strdup_printf ("DELL-%s" G_GUINT64_FORMAT, tpm_guid_alt);

	g_debug ("Creating primary TPM GUID %s and secondary TPM GUID %s",
		 tpm_guid_raw, tpm_guid_raw_alt);
	version_str = fu_common_version_from_uint32 (out->fw_version,
						     FWUPD_VERSION_FORMAT_QUAD);

	/* make it clear that the TPM is a discrete device of the product */
	if (!data->smi_obj->fake_smbios) {
		product_name = fu_plugin_get_dmi_value (plugin, FU_HWIDS_KEY_PRODUCT_NAME);
	}
	pretty_tpm_name = g_strdup_printf ("%s TPM %s", product_name, tpm_mode);
	pretty_tpm_name_alt = g_strdup_printf ("%s TPM %s", product_name, tpm_mode_alt);

	/* build Standard device nodes */
	dev = fu_device_new ();
	fu_device_set_id (dev, tpm_id);
	fu_device_add_guid (dev, tpm_guid);
	fu_device_set_vendor (dev, "Dell Inc.");
	fu_device_set_name (dev, pretty_tpm_name);
	fu_device_set_summary (dev, "Platform TPM device");
	fu_device_set_version (dev, version_str, FWUPD_VERSION_FORMAT_QUAD);
	fu_device_add_flag (dev, FWUPD_DEVICE_FLAG_INTERNAL);
	fu_device_add_flag (dev, FWUPD_DEVICE_FLAG_REQUIRE_AC);
	fu_device_add_icon (dev, "computer");
	fu_device_set_metadata (dev, FU_DEVICE_METADATA_UEFI_DEVICE_KIND, "dell-tpm-firmware");
	if ((out->status & TPM_OWN_MASK) == 0 && out->flashes_left > 0) {
		if (fu_plugin_dell_capsule_supported (plugin)) {
			fu_device_add_flag (dev, FWUPD_DEVICE_FLAG_UPDATABLE);
			fu_device_add_flag (dev, FWUPD_DEVICE_FLAG_NEEDS_REBOOT);
		} else {
			fu_device_set_update_error (dev,
						    "UEFI capsule updates turned off in BIOS setup");
		}
		fu_device_set_flashes_left (dev, out->flashes_left);
	} else {
		g_debug ("%s updating disabled due to TPM ownership",
			pretty_tpm_name);
	}
	fu_plugin_device_register (plugin, dev);

	/* build alternate device node */
	if (can_switch_modes) {
		dev_alt = fu_device_new ();
		fu_device_set_id (dev_alt, tpm_id_alt);
		fu_device_add_guid (dev_alt, tpm_guid_alt);
		fu_device_set_vendor (dev, "Dell Inc.");
		fu_device_set_name (dev_alt, pretty_tpm_name_alt);
		fu_device_set_summary (dev_alt, "Alternate mode for platform TPM device");
		fu_device_add_flag (dev_alt, FWUPD_DEVICE_FLAG_INTERNAL);
		fu_device_add_flag (dev_alt, FWUPD_DEVICE_FLAG_REQUIRE_AC);
		fu_device_add_flag (dev_alt, FWUPD_DEVICE_FLAG_LOCKED);
		fu_device_add_icon (dev_alt, "computer");
		fu_device_set_alternate_id (dev_alt, fu_device_get_id (dev));
		fu_device_set_metadata (dev_alt, FU_DEVICE_METADATA_UEFI_DEVICE_KIND, "dell-tpm-firmware");
		fu_device_add_parent_guid (dev_alt, tpm_guid);

		/* If TPM is not owned and at least 1 flash left allow mode switching
		 *
		 * Mode switching is turned on by setting flashes left on alternate
		 * device.
		 */
		if ((out->status & TPM_OWN_MASK) == 0 && out->flashes_left > 0) {
			fu_device_set_flashes_left (dev_alt, out->flashes_left);
		} else {
			g_debug ("%s mode switch disabled due to TPM ownership",
				 pretty_tpm_name);
		}
		fu_plugin_device_register (plugin, dev_alt);
	}
	else
		g_debug ("System %04x does not offer TPM modeswitching",
			system_id);

	return TRUE;
}

void
fu_plugin_device_registered (FuPlugin *plugin, FuDevice *device)
{
	/* thunderbolt plugin */
	if (g_strcmp0 (fu_device_get_plugin (device), "thunderbolt") == 0 &&
	    fu_device_has_flag (device, FWUPD_DEVICE_FLAG_INTERNAL)) {
		/* fix VID/DID of safe mode devices */
		if (fu_device_get_metadata_boolean (device, FU_DEVICE_METADATA_TBT_IS_SAFE_MODE)) {
			g_autofree gchar *vendor_id = NULL;
			g_autofree gchar *device_id = NULL;
			guint16 system_id = 0;

			vendor_id = g_strdup ("TBT:0x00D4");
			system_id = fu_dell_get_system_id (plugin);
			if (system_id == 0)
				return;
			/* the kernel returns lowercase in sysfs, need to match it */
			device_id = g_strdup_printf ("TBT-%04x%04x", 0x00d4u,
						     (unsigned) system_id);
			fu_device_set_vendor_id (device, vendor_id);
			fu_device_add_instance_id (device, device_id);
			fu_device_add_flag (device, FWUPD_DEVICE_FLAG_UPDATABLE);
		}
	}
}

static gboolean
fu_dell_toggle_flash (FuPlugin *plugin, FuDevice *device,
		      gboolean enable, GError **error)
{
	FuPluginData *data = fu_plugin_get_data (plugin);
	gboolean has_host = fu_dell_host_mst_supported (plugin);
	gboolean has_dock;
	guint32 dock_location;
	const gchar *tmp;

	if (device) {
		if (!fu_device_has_flag (device, FWUPD_DEVICE_FLAG_UPDATABLE))
			return TRUE;
		tmp = fu_device_get_plugin (device);
		if (g_strcmp0 (tmp, "synapticsmst") != 0)
			return TRUE;
		g_debug ("preparing/cleaning update for %s", tmp);
	}

	/* Dock MST Hub */
	has_dock = fu_dell_detect_dock (data->smi_obj, &dock_location);
	if (has_dock) {
		if (!fu_dell_toggle_dock_mode (data->smi_obj, enable,
					       dock_location, error))
			g_debug ("unable to change dock to %d", enable);
		else
			g_debug ("Toggled dock mode to %d", enable);
	}

	/* System MST hub */
	if (has_host) {
		if (!fu_dell_toggle_host_mode (data->smi_obj, MST_GPIO_GUID, enable))
			g_debug ("Unable to toggle MST hub GPIO to %d", enable);
		else
			g_debug ("Toggled MST hub GPIO to %d", enable);
	}

#if defined (HAVE_SYNAPTICS)
	/* set a delay to allow OS response to settling the GPIO change */
	if (enable && device == NULL && (has_dock || has_host))
		fu_plugin_set_coldplug_delay (plugin, DELL_FLASH_MODE_DELAY * 1000);
#endif
	return TRUE;
}

gboolean
fu_plugin_update_prepare (FuPlugin *plugin,
			  FwupdInstallFlags flags,
			  FuDevice *device,
			  GError **error)
{

	return fu_dell_toggle_flash (plugin, device, TRUE, error);
}

gboolean
fu_plugin_update_cleanup (FuPlugin *plugin,
			  FwupdInstallFlags flags,
			  FuDevice *device,
			  GError **error)
{
	return fu_dell_toggle_flash (plugin, device , FALSE, error);
}

gboolean
fu_plugin_coldplug_prepare (FuPlugin *plugin, GError **error)
{
	return fu_dell_toggle_flash (plugin, NULL, TRUE, error);
}

gboolean
fu_plugin_coldplug_cleanup (FuPlugin *plugin, GError **error)
{
	return fu_dell_toggle_flash (plugin, NULL, FALSE, error);
}

void
fu_plugin_init (FuPlugin *plugin)
{
	FuPluginData *data = fu_plugin_alloc_data (plugin, sizeof (FuPluginData));
	g_autofree gchar *tmp = NULL;

	fu_plugin_set_build_hash (plugin, FU_BUILD_HASH);
	tmp = g_strdup_printf ("%d.%d",
			       smbios_get_library_version_major(),
			       smbios_get_library_version_minor());
	fu_plugin_add_runtime_version (plugin, "com.dell.libsmbios", tmp);
	g_debug ("Using libsmbios %s", tmp);

	data->smi_obj = g_malloc0 (sizeof (FuDellSmiObj));
	if (g_getenv ("FWUPD_DELL_VERBOSE") != NULL)
		g_setenv ("LIBSMBIOS_C_DEBUG_OUTPUT_ALL", "1", TRUE);
	if (fu_dell_supported (plugin))
		data->smi_obj->smi = dell_smi_factory (DELL_SMI_DEFAULTS);
	data->smi_obj->fake_smbios = FALSE;
	if (g_getenv ("FWUPD_DELL_FAKE_SMBIOS") != NULL)
		data->smi_obj->fake_smbios = TRUE;
	fu_plugin_add_rule (plugin, FU_PLUGIN_RULE_REQUIRES_QUIRK, FU_QUIRKS_PLUGIN);

	/* make sure that UEFI plugin is ready to receive devices */
	fu_plugin_add_rule (plugin, FU_PLUGIN_RULE_RUN_AFTER, "uefi");
}

void
fu_plugin_destroy (FuPlugin *plugin)
{
	FuPluginData *data = fu_plugin_get_data (plugin);
	if (data->smi_obj->smi)
		dell_smi_obj_free (data->smi_obj->smi);
	g_free(data->smi_obj);
}

gboolean
fu_plugin_startup (FuPlugin *plugin, GError **error)
{
	FuPluginData *data = fu_plugin_get_data (plugin);
	g_autofree gchar *sysfsfwdir = NULL;
	g_autofree gchar *esrtdir = NULL;

	if (data->smi_obj->fake_smbios) {
		g_debug ("Called with fake SMBIOS implementation. "
			 "We're ignoring test for SBMIOS table and ESRT. "
			 "Individual calls will need to be properly staged.");
		return TRUE;
	}

	if (!fu_dell_supported (plugin)) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "Firmware updating not supported");
		return FALSE;
	}

	if (data->smi_obj->smi == NULL) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "failed to initialize libsmbios library");
		return FALSE;
	}

	/* If ESRT is not turned on, fwupd will have already created an
	 * unlock device.
	 *
	 * Once unlocked, that will enable flashing capsules here too.
	 */
	sysfsfwdir = fu_common_get_path (FU_PATH_KIND_SYSFSDIR_FW);
	esrtdir = g_build_filename (sysfsfwdir, "efi", "esrt", NULL);
	if (g_file_test (esrtdir, G_FILE_TEST_EXISTS)) {
		data->capsule_supported = TRUE;
	} else {
		g_debug ("UEFI capsule firmware updating not supported");
	}

	return TRUE;
}

static gboolean
fu_plugin_dell_coldplug (FuPlugin *plugin, GError **error)
{
	/* look for switchable TPM */
	if (!fu_plugin_dell_detect_tpm (plugin, error))
		g_debug ("No switchable TPM detected");
	return TRUE;
}

gboolean
fu_plugin_coldplug (FuPlugin *plugin, GError **error)
{
	return fu_plugin_dell_coldplug (plugin, error);
}

gboolean
fu_plugin_recoldplug (FuPlugin *plugin, GError **error)
{
	return fu_plugin_dell_coldplug (plugin, error);
}
