/*
 * Copyright 2018 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#define G_LOG_DOMAIN "FuIdle"

#include "config.h"

#include "fu-idle.h"

static void
fu_idle_finalize(GObject *obj);

struct _FuIdle {
	GObject parent_instance;
	GPtrArray *items; /* of FuIdleItem */
	guint idle_id;
	guint timeout;
	FuIdleInhibit inhibit_old;
};

enum { SIGNAL_INHIBIT_CHANGED, SIGNAL_TIMEOUT, SIGNAL_LAST };

static guint signals[SIGNAL_LAST] = {0};

typedef struct {
	FuIdleInhibit inhibit;
	gchar *reason;
	guint32 token;
} FuIdleItem;

G_DEFINE_TYPE(FuIdle, fu_idle, G_TYPE_OBJECT)

static gboolean
fu_idle_check_cb(gpointer user_data)
{
	FuIdle *self = FU_IDLE(user_data);
	g_signal_emit(self, signals[SIGNAL_TIMEOUT], 0);
	return G_SOURCE_CONTINUE;
}

static void
fu_idle_start(FuIdle *self)
{
	if (self->idle_id != 0)
		return;
	if (self->timeout == 0)
		return;
	self->idle_id = g_timeout_add_seconds(self->timeout, fu_idle_check_cb, self);
}

static void
fu_idle_stop(FuIdle *self)
{
	if (self->idle_id == 0)
		return;
	g_clear_handle_id(&self->idle_id, g_source_remove);
}

static void
fu_idle_emit_inhibit_changed(FuIdle *self)
{
	FuIdleInhibit inhibit_global = FU_IDLE_INHIBIT_NONE;

	fu_idle_reset(self);
	for (guint i = 0; i < self->items->len; i++) {
		FuIdleItem *item = g_ptr_array_index(self->items, i);
		inhibit_global |= item->inhibit;
	}
	if (self->inhibit_old != inhibit_global) {
		g_autofree gchar *inhibit_str = fu_idle_inhibit_to_string(inhibit_global);
		g_debug("now inhibited: %s", inhibit_str);
		g_signal_emit(self, signals[SIGNAL_INHIBIT_CHANGED], 0, inhibit_global);
		self->inhibit_old = inhibit_global;
	}
}

void
fu_idle_reset(FuIdle *self)
{
	g_return_if_fail(FU_IS_IDLE(self));
	fu_idle_stop(self);
	if (!fu_idle_has_inhibit(self, FU_IDLE_INHIBIT_TIMEOUT))
		fu_idle_start(self);
}

void
fu_idle_uninhibit(FuIdle *self, guint32 token)
{
	g_return_if_fail(FU_IS_IDLE(self));
	g_return_if_fail(token != 0);

	for (guint i = 0; i < self->items->len; i++) {
		FuIdleItem *item = g_ptr_array_index(self->items, i);
		if (item->token == token) {
			g_autofree gchar *inhibit_str = fu_idle_inhibit_to_string(item->inhibit);
			g_debug("uninhibiting: %s by %s", inhibit_str, item->reason);
			g_ptr_array_remove_index(self->items, i);
			break;
		}
	}
	fu_idle_emit_inhibit_changed(self);
}

guint32
fu_idle_inhibit(FuIdle *self, FuIdleInhibit inhibit, const gchar *reason)
{
	FuIdleItem *item;
	g_autofree gchar *inhibit_str = fu_idle_inhibit_to_string(inhibit);
	g_autoptr(GByteArray) buf = NULL;

	g_return_val_if_fail(FU_IS_IDLE(self), 0);
	g_return_val_if_fail(inhibit != FU_IDLE_INHIBIT_NONE, 0);

	g_debug("inhibiting: %s by %s", inhibit_str, reason);
	item = g_new0(FuIdleItem, 1);
	item->inhibit = inhibit;
	item->reason = g_strdup(reason);

	/* prefer a cryptographically secure RNG, fall back if not available */
	buf = fu_common_get_random(sizeof(guint32), NULL);
	if (buf != NULL)
		item->token = fu_memread_uint32(buf->data, G_LITTLE_ENDIAN);
	if (item->token == 0) {
		/* nocheck:blocked */
		item->token = g_random_int_range(1, G_MAXINT);
	}

	g_ptr_array_add(self->items, item);

	fu_idle_emit_inhibit_changed(self);
	return item->token;
}

gboolean
fu_idle_has_inhibit(FuIdle *self, FuIdleInhibit inhibit)
{
	g_return_val_if_fail(FU_IS_IDLE(self), FALSE);
	g_return_val_if_fail(inhibit != FU_IDLE_INHIBIT_NONE, FALSE);

	for (guint i = 0; i < self->items->len; i++) {
		FuIdleItem *item = g_ptr_array_index(self->items, i);
		if (item->inhibit & inhibit)
			return TRUE;
	}
	return FALSE;
}

void
fu_idle_set_timeout(FuIdle *self, guint timeout)
{
	g_return_if_fail(FU_IS_IDLE(self));
	g_debug("setting timeout to %us", timeout);
	self->timeout = timeout;
	fu_idle_reset(self);
}

static void
fu_idle_item_free(FuIdleItem *item)
{
	g_free(item->reason);
	g_free(item);
}

FuIdleLocker *
fu_idle_locker_new(FuIdle *self, FuIdleInhibit inhibit, const gchar *reason)
{
	FuIdleLocker *locker = g_new0(FuIdleLocker, 1);
	locker->idle = g_object_ref(self);
	locker->token = fu_idle_inhibit(self, inhibit, reason);
	return locker;
}

void
fu_idle_locker_free(FuIdleLocker *locker)
{
	g_return_if_fail(locker != NULL);
	fu_idle_uninhibit(locker->idle, locker->token);
	g_object_unref(locker->idle);
	g_free(locker);
}

static void
fu_idle_class_init(FuIdleClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS(klass);

	object_class->finalize = fu_idle_finalize;

	signals[SIGNAL_INHIBIT_CHANGED] = g_signal_new("inhibit-changed",
						       G_TYPE_FROM_CLASS(object_class),
						       G_SIGNAL_RUN_LAST,
						       0,
						       NULL,
						       NULL,
						       g_cclosure_marshal_VOID__UINT,
						       G_TYPE_NONE,
						       1,
						       G_TYPE_UINT);
	signals[SIGNAL_TIMEOUT] = g_signal_new("timeout",
					       G_TYPE_FROM_CLASS(object_class),
					       G_SIGNAL_RUN_LAST,
					       0,
					       NULL,
					       NULL,
					       g_cclosure_marshal_VOID__VOID,
					       G_TYPE_NONE,
					       0);
}

static void
fu_idle_init(FuIdle *self)
{
	self->items = g_ptr_array_new_with_free_func((GDestroyNotify)fu_idle_item_free);
}

static void
fu_idle_finalize(GObject *obj)
{
	FuIdle *self = FU_IDLE(obj);

	fu_idle_stop(self);
	g_ptr_array_unref(self->items);

	G_OBJECT_CLASS(fu_idle_parent_class)->finalize(obj);
}

FuIdle *
fu_idle_new(void)
{
	FuIdle *self;
	self = g_object_new(FU_TYPE_IDLE, NULL);
	return FU_IDLE(self);
}
