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

#define G_LOG_DOMAIN "FuIdle"

#include "config.h"

#include "fu-idle.h"
#include "fu-mutex.h"

static void
fu_idle_finalize(GObject *obj);

struct _FuIdle {
	GObject parent_instance;
	GPtrArray *items; /* of FuIdleItem */
	GRWLock items_mutex;
	guint idle_id;
	guint timeout;
	FwupdStatus status;
};

enum { PROP_0, PROP_STATUS, PROP_LAST };

static void
fu_idle_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
	FuIdle *self = FU_IDLE(object);
	switch (prop_id) {
	case PROP_STATUS:
		g_value_set_uint(value, self->status);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
		break;
	}
}

static void
fu_idle_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
	switch (prop_id) {
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
		break;
	}
}

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

G_DEFINE_TYPE(FuIdle, fu_idle, G_TYPE_OBJECT)

FwupdStatus
fu_idle_get_status(FuIdle *self)
{
	g_return_val_if_fail(FU_IS_IDLE(self), FWUPD_STATUS_UNKNOWN);
	return self->status;
}

static void
fu_idle_set_status(FuIdle *self, FwupdStatus status)
{
	if (self->status == status)
		return;
	self->status = status;
	g_debug("status now %s", fwupd_status_to_string(status));
	g_object_notify(G_OBJECT(self), "status");
}

static gboolean
fu_idle_check_cb(gpointer user_data)
{
	FuIdle *self = FU_IDLE(user_data);
	fu_idle_set_status(self, FWUPD_STATUS_SHUTDOWN);
	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_source_remove(self->idle_id);
	self->idle_id = 0;
}

void
fu_idle_reset(FuIdle *self)
{
	g_return_if_fail(FU_IS_IDLE(self));
	fu_idle_stop(self);
	if (self->items->len == 0)
		fu_idle_start(self);
}

void
fu_idle_uninhibit(FuIdle *self, guint32 token)
{
	g_autoptr(GRWLockWriterLocker) locker = g_rw_lock_writer_locker_new(&self->items_mutex);

	g_return_if_fail(FU_IS_IDLE(self));
	g_return_if_fail(token != 0);
	g_return_if_fail(locker != NULL);

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

guint32
fu_idle_inhibit(FuIdle *self, const gchar *reason)
{
	FuIdleItem *item;
	g_autoptr(GRWLockWriterLocker) locker = g_rw_lock_writer_locker_new(&self->items_mutex);

	g_return_val_if_fail(FU_IS_IDLE(self), 0);
	g_return_val_if_fail(reason != NULL, 0);
	g_return_val_if_fail(locker != NULL, 0);

	g_debug("inhibiting: %s", reason);
	item = g_new0(FuIdleItem, 1);
	item->reason = g_strdup(reason);
	item->token = g_random_int_range(1, G_MAXINT);
	g_ptr_array_add(self->items, item);
	fu_idle_reset(self);
	return item->token;
}

gboolean
fu_idle_has_inhibit(FuIdle *self, const gchar *reason)
{
	g_autoptr(GRWLockWriterLocker) locker = g_rw_lock_reader_locker_new(&self->items_mutex);

	g_return_val_if_fail(FU_IS_IDLE(self), FALSE);
	g_return_val_if_fail(reason != NULL, FALSE);
	g_return_val_if_fail(locker != NULL, FALSE);

	for (guint i = 0; i < self->items->len; i++) {
		FuIdleItem *item = g_ptr_array_index(self->items, i);
		if (g_strcmp0(item->reason, reason) == 0)
			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, const gchar *reason)
{
	FuIdleLocker *locker = g_new0(FuIdleLocker, 1);
	locker->idle = g_object_ref(self);
	locker->token = fu_idle_inhibit(self, reason);
	return locker;
}

void
fu_idle_locker_free(FuIdleLocker *locker)
{
	if (locker == NULL)
		return;
	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);
	GParamSpec *pspec;

	object_class->finalize = fu_idle_finalize;
	object_class->get_property = fu_idle_get_property;
	object_class->set_property = fu_idle_set_property;

	/**
	 * FuIdle:status:
	 *
	 * The status of the idle monitor.
	 */
	pspec = g_param_spec_uint("status",
				  NULL,
				  NULL,
				  FWUPD_STATUS_UNKNOWN,
				  FWUPD_STATUS_LAST,
				  FWUPD_STATUS_UNKNOWN,
				  G_PARAM_READABLE | G_PARAM_STATIC_NAME);
	g_object_class_install_property(object_class, PROP_STATUS, pspec);
}

static void
fu_idle_init(FuIdle *self)
{
	self->status = FWUPD_STATUS_IDLE;
	self->items = g_ptr_array_new_with_free_func((GDestroyNotify)fu_idle_item_free);
	g_rw_lock_init(&self->items_mutex);
}

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

	fu_idle_stop(self);
	g_rw_lock_clear(&self->items_mutex);
	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);
}
