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

#define G_LOG_DOMAIN "FuBackend"

#include "config.h"

#include <gudev/gudev.h>

#include "fu-udev-backend.h"
#include "fu-udev-device.h"

struct _FuUdevBackend {
	FuBackend parent_instance;
	GUdevClient *gudev_client;
	GHashTable *changed_idle_ids; /* sysfs:FuUdevBackendHelper */
	GPtrArray *subsystems;
};

G_DEFINE_TYPE(FuUdevBackend, fu_udev_backend, FU_TYPE_BACKEND)

static void
fu_udev_backend_device_add(FuUdevBackend *self, GUdevDevice *udev_device)
{
	g_autoptr(FuUdevDevice) device = NULL;

	/* success */
	device =
	    fu_udev_device_new_with_context(fu_backend_get_context(FU_BACKEND(self)), udev_device);
	fu_backend_device_added(FU_BACKEND(self), FU_DEVICE(device));
}

static void
fu_udev_backend_device_remove(FuUdevBackend *self, GUdevDevice *udev_device)
{
	FuDevice *device_tmp;

	/* find the device we enumerated */
	device_tmp =
	    fu_backend_lookup_by_id(FU_BACKEND(self), g_udev_device_get_sysfs_path(udev_device));
	if (device_tmp != NULL) {
		if (g_getenv("FWUPD_PROBE_VERBOSE") != NULL) {
			g_debug("UDEV %s removed", g_udev_device_get_sysfs_path(udev_device));
		}
		fu_backend_device_removed(FU_BACKEND(self), device_tmp);
	}
}

typedef struct {
	FuUdevBackend *self;
	FuDevice *device;
	guint idle_id;
} FuUdevBackendHelper;

static void
fu_udev_backend_changed_helper_free(FuUdevBackendHelper *helper)
{
	if (helper->idle_id != 0)
		g_source_remove(helper->idle_id);
	g_object_unref(helper->self);
	g_object_unref(helper->device);
	g_free(helper);
}

static FuUdevBackendHelper *
fu_udev_backend_changed_helper_new(FuUdevBackend *self, FuDevice *device)
{
	FuUdevBackendHelper *helper = g_new0(FuUdevBackendHelper, 1);
	helper->self = g_object_ref(self);
	helper->device = g_object_ref(device);
	return helper;
}

static gboolean
fu_udev_backend_device_changed_cb(gpointer user_data)
{
	FuUdevBackendHelper *helper = (FuUdevBackendHelper *)user_data;
	fu_backend_device_changed(FU_BACKEND(helper->self), helper->device);
	helper->idle_id = 0;
	g_hash_table_remove(helper->self->changed_idle_ids,
			    fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(helper->device)));
	return FALSE;
}

static void
fu_udev_backend_device_changed(FuUdevBackend *self, GUdevDevice *udev_device)
{
	const gchar *sysfs_path = g_udev_device_get_sysfs_path(udev_device);
	FuUdevBackendHelper *helper;
	FuDevice *device_tmp;

	/* not a device we enumerated */
	device_tmp = fu_backend_lookup_by_id(FU_BACKEND(self), sysfs_path);
	if (device_tmp == NULL)
		return;

	/* run all plugins, with per-device rate limiting */
	if (g_hash_table_remove(self->changed_idle_ids, sysfs_path)) {
		g_debug("re-adding rate-limited timeout for %s", sysfs_path);
	} else {
		g_debug("adding rate-limited timeout for %s", sysfs_path);
	}
	helper = fu_udev_backend_changed_helper_new(self, device_tmp);
	helper->idle_id = g_timeout_add(500, fu_udev_backend_device_changed_cb, helper);
	g_hash_table_insert(self->changed_idle_ids, g_strdup(sysfs_path), helper);
}

static void
fu_udev_backend_uevent_cb(GUdevClient *gudev_client,
			  const gchar *action,
			  GUdevDevice *udev_device,
			  FuUdevBackend *self)
{
	if (g_strcmp0(action, "add") == 0) {
		fu_udev_backend_device_add(self, udev_device);
		return;
	}
	if (g_strcmp0(action, "remove") == 0) {
		fu_udev_backend_device_remove(self, udev_device);
		return;
	}
	if (g_strcmp0(action, "change") == 0) {
		fu_udev_backend_device_changed(self, udev_device);
		return;
	}
}

static gboolean
fu_udev_backend_coldplug(FuBackend *backend, GError **error)
{
	FuUdevBackend *self = FU_UDEV_BACKEND(backend);

	/* udev watches can only be set up in _init() so set up client now */
	if (self->subsystems->len > 0) {
		g_auto(GStrv) subsystems = g_new0(gchar *, self->subsystems->len + 1);
		for (guint i = 0; i < self->subsystems->len; i++) {
			const gchar *subsystem = g_ptr_array_index(self->subsystems, i);
			subsystems[i] = g_strdup(subsystem);
		}
		self->gudev_client = g_udev_client_new((const gchar *const *)subsystems);
		g_signal_connect(G_UDEV_CLIENT(self->gudev_client),
				 "uevent",
				 G_CALLBACK(fu_udev_backend_uevent_cb),
				 self);
	}

	/* get all devices of class */
	for (guint i = 0; i < self->subsystems->len; i++) {
		const gchar *subsystem = g_ptr_array_index(self->subsystems, i);
		GList *devices = g_udev_client_query_by_subsystem(self->gudev_client, subsystem);
		if (g_getenv("FWUPD_PROBE_VERBOSE") != NULL) {
			g_debug("%u devices with subsystem %s", g_list_length(devices), subsystem);
		}
		for (GList *l = devices; l != NULL; l = l->next) {
			GUdevDevice *udev_device = l->data;
			fu_udev_backend_device_add(self, udev_device);
		}
		g_list_foreach(devices, (GFunc)g_object_unref, NULL);
		g_list_free(devices);
	}

	return TRUE;
}

static void
fu_udev_backend_finalize(GObject *object)
{
	FuUdevBackend *self = FU_UDEV_BACKEND(object);
	if (self->gudev_client != NULL)
		g_object_unref(self->gudev_client);
	if (self->subsystems != NULL)
		g_ptr_array_unref(self->subsystems);
	g_hash_table_unref(self->changed_idle_ids);
	G_OBJECT_CLASS(fu_udev_backend_parent_class)->finalize(object);
}

static void
fu_udev_backend_init(FuUdevBackend *self)
{
	self->changed_idle_ids =
	    g_hash_table_new_full(g_str_hash,
				  g_str_equal,
				  g_free,
				  (GDestroyNotify)fu_udev_backend_changed_helper_free);
}

static void
fu_udev_backend_class_init(FuUdevBackendClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
	FuBackendClass *klass_backend = FU_BACKEND_CLASS(klass);
	object_class->finalize = fu_udev_backend_finalize;
	klass_backend->coldplug = fu_udev_backend_coldplug;
}

FuBackend *
fu_udev_backend_new(GPtrArray *subsystems)
{
	FuUdevBackend *self;
	self = FU_UDEV_BACKEND(g_object_new(FU_TYPE_UDEV_BACKEND, "name", "udev", NULL));
	if (subsystems != NULL)
		self->subsystems = g_ptr_array_ref(subsystems);
	return FU_BACKEND(self);
}
