/*
 * SPDX-License-Identifier: LGPL-2.0-or-later
 */

/**
 * SECTION: e-source-refresh
 * @include: libedataserver/libedataserver.h
 * @short_description: #ESource extension for refresh settings
 *
 * The #ESourceRefresh extension tracks the interval for fetching
 * updates from a remote server.
 *
 * Access the extension as follows:
 *
 * |[
 *   #include <libedataserver/libedataserver.h>
 *
 *   ESourceRefresh *extension;
 *
 *   extension = e_source_get_extension (source, E_SOURCE_EXTENSION_REFRESH);
 * ]|
 **/

#include "e-source-refresh.h"

typedef struct _TimeoutNode TimeoutNode;

struct _ESourceRefreshPrivate {
	gboolean enabled;
	gboolean enabled_on_metered_network;
	guint interval_minutes;

	GMutex timeout_lock;
	GHashTable *timeout_table;
	guint next_timeout_id;
};

struct _TimeoutNode {
	GSource *source;
	GMainContext *context;
	ESourceRefresh *extension;  /* not referenced */

	ESourceRefreshFunc callback;
	gpointer user_data;
	GDestroyNotify notify;
};

enum {
	PROP_0,
	PROP_ENABLED,
	PROP_ENABLED_ON_METERED_NETWORK,
	PROP_INTERVAL_MINUTES,
	N_PROPS
};

static GParamSpec *properties[N_PROPS] = { NULL, };

G_DEFINE_TYPE_WITH_PRIVATE (
	ESourceRefresh,
	e_source_refresh,
	E_TYPE_SOURCE_EXTENSION)

static TimeoutNode *
timeout_node_new (ESourceRefresh *extension,
                  GMainContext *context,
                  ESourceRefreshFunc callback,
                  gpointer user_data,
                  GDestroyNotify notify)
{
	TimeoutNode *node;

	if (context != NULL)
		g_main_context_ref (context);

	node = g_slice_new0 (TimeoutNode);
	node->context = context;
	node->callback = callback;
	node->user_data = user_data;
	node->notify = notify;

	/* Do not reference.  The timeout node will
	 * not outlive the ESourceRefresh extension. */
	node->extension = extension;

	return node;
}

static gboolean
timeout_node_invoke (gpointer data)
{
	TimeoutNode *node = data;
	ESourceExtension *extension;
	ESource *source;

	extension = E_SOURCE_EXTENSION (node->extension);
	source = e_source_extension_ref_source (extension);
	g_return_val_if_fail (source != NULL, FALSE);

	/* We allow timeouts to be scheduled for disabled data sources
	 * but we don't invoke the callback.  Keeps the logic simple. */
	if (e_source_get_enabled (source))
		node->callback (source, node->user_data);

	g_object_unref (source);

	return TRUE;
}

static void
timeout_node_attach (TimeoutNode *node)
{
	guint interval_minutes;

	if (node->source != NULL)
		return;

	interval_minutes = e_source_refresh_get_interval_minutes (node->extension);
	if (interval_minutes > 0) {
		node->source = g_timeout_source_new_seconds (interval_minutes * 60);

		g_source_set_callback (
			node->source,
			timeout_node_invoke,
			node,
			(GDestroyNotify) NULL);

		g_source_attach (node->source, node->context);
	}
}

static void
timeout_node_detach (TimeoutNode *node)
{
	if (node->source == NULL)
		return;

	g_source_destroy (node->source);
	g_source_unref (node->source);
	node->source = NULL;
}

static void
timeout_node_free (TimeoutNode *node)
{
	if (node->source != NULL)
		timeout_node_detach (node);

	if (node->context != NULL)
		g_main_context_unref (node->context);

	if (node->notify != NULL)
		node->notify (node->user_data);

	g_slice_free (TimeoutNode, node);
}

static void
source_refresh_update_timeouts (ESourceRefresh *extension,
                                gboolean invoke_callbacks)
{
	GList *list, *link;

	g_mutex_lock (&extension->priv->timeout_lock);

	list = g_hash_table_get_values (extension->priv->timeout_table);

	for (link = list; link != NULL; link = g_list_next (link)) {
		TimeoutNode *node = link->data;

		timeout_node_detach (node);

		if (invoke_callbacks)
			timeout_node_invoke (node);

		if (e_source_refresh_get_enabled (extension))
			timeout_node_attach (node);
	}

	g_list_free (list);

	g_mutex_unlock (&extension->priv->timeout_lock);
}

static gboolean
source_refresh_idle_cb (gpointer user_data)
{
	ESource *source = E_SOURCE (user_data);

	if (e_source_get_enabled (source))
		e_source_refresh_force_timeout (source);

	return FALSE;
}

static void
source_refresh_notify_enabled_cb (ESource *source,
                                  GParamSpec *pspec,
                                  ESourceRefresh *extension)
{
	GSource *idle_source;
	GMainContext *main_context;

	main_context = e_source_ref_main_context (source);

	idle_source = g_idle_source_new ();
	g_source_set_callback (
		idle_source,
		source_refresh_idle_cb,
		g_object_ref (source),
		(GDestroyNotify) g_object_unref);
	g_source_attach (idle_source, main_context);
	g_source_unref (idle_source);

	g_main_context_unref (main_context);
}

static void
source_refresh_set_property (GObject *object,
                             guint property_id,
                             const GValue *value,
                             GParamSpec *pspec)
{
	switch (property_id) {
		case PROP_ENABLED:
			e_source_refresh_set_enabled (
				E_SOURCE_REFRESH (object),
				g_value_get_boolean (value));
			return;

		case PROP_ENABLED_ON_METERED_NETWORK:
			e_source_refresh_set_enabled_on_metered_network (
				E_SOURCE_REFRESH (object),
				g_value_get_boolean (value));
			return;

		case PROP_INTERVAL_MINUTES:
			e_source_refresh_set_interval_minutes (
				E_SOURCE_REFRESH (object),
				g_value_get_uint (value));
			return;
	}

	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
source_refresh_get_property (GObject *object,
                             guint property_id,
                             GValue *value,
                             GParamSpec *pspec)
{
	switch (property_id) {
		case PROP_ENABLED:
			g_value_set_boolean (
				value,
				e_source_refresh_get_enabled (
				E_SOURCE_REFRESH (object)));
			return;

		case PROP_ENABLED_ON_METERED_NETWORK:
			g_value_set_boolean (
				value,
				e_source_refresh_get_enabled_on_metered_network (
				E_SOURCE_REFRESH (object)));
			return;

		case PROP_INTERVAL_MINUTES:
			g_value_set_uint (
				value,
				e_source_refresh_get_interval_minutes (
				E_SOURCE_REFRESH (object)));
			return;
	}

	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
source_refresh_dispose (GObject *object)
{
	ESourceRefreshPrivate *priv;

	priv = E_SOURCE_REFRESH (object)->priv;

	g_hash_table_remove_all (priv->timeout_table);

	/* Chain up to parent's dispose() method. */
	G_OBJECT_CLASS (e_source_refresh_parent_class)->dispose (object);
}

static void
source_refresh_finalize (GObject *object)
{
	ESourceRefreshPrivate *priv;

	priv = E_SOURCE_REFRESH (object)->priv;

	g_mutex_clear (&priv->timeout_lock);
	g_hash_table_destroy (priv->timeout_table);

	/* Chain up to parent's finalize() method. */
	G_OBJECT_CLASS (e_source_refresh_parent_class)->finalize (object);
}

static void
source_refresh_constructed (GObject *object)
{
	ESourceExtension *extension;
	ESource *source;

	/* Chain up to parent's constructed() method. */
	G_OBJECT_CLASS (e_source_refresh_parent_class)->constructed (object);

	extension = E_SOURCE_EXTENSION (object);
	source = e_source_extension_ref_source (extension);

	/* There should be no lifecycle issues here
	 * since we get finalized with our ESource. */
	g_signal_connect (
		source, "notify::enabled",
		G_CALLBACK (source_refresh_notify_enabled_cb),
		extension);

	g_object_unref (source);
}

static void
e_source_refresh_class_init (ESourceRefreshClass *class)
{
	GObjectClass *object_class;
	ESourceExtensionClass *extension_class;

	object_class = G_OBJECT_CLASS (class);
	object_class->set_property = source_refresh_set_property;
	object_class->get_property = source_refresh_get_property;
	object_class->dispose = source_refresh_dispose;
	object_class->finalize = source_refresh_finalize;
	object_class->constructed = source_refresh_constructed;

	extension_class = E_SOURCE_EXTENSION_CLASS (class);
	extension_class->name = E_SOURCE_EXTENSION_REFRESH;

	/**
	 * ESourceRefresh:enabled
	 *
	 * Whether to periodically refresh
	 **/
	properties[PROP_ENABLED] =
		g_param_spec_boolean (
			"enabled",
			NULL, NULL,
			TRUE,
			G_PARAM_READWRITE |
			G_PARAM_CONSTRUCT |
			G_PARAM_EXPLICIT_NOTIFY |
			G_PARAM_STATIC_STRINGS |
			E_SOURCE_PARAM_SETTING);

	/**
	 * ESourceRefresh:enabled-on-metered-network
	 *
	 * Whether to enable refresh on metered network
	 **/
	properties[PROP_ENABLED_ON_METERED_NETWORK] =
		g_param_spec_boolean (
			"enabled-on-metered-network",
			NULL, NULL,
			TRUE,
			G_PARAM_READWRITE |
			G_PARAM_CONSTRUCT |
			G_PARAM_EXPLICIT_NOTIFY |
			G_PARAM_STATIC_STRINGS |
			E_SOURCE_PARAM_SETTING);

	/**
	 * ESourceRefresh:interval-minutes
	 *
	 * Refresh interval in minutes
	 **/
	properties[PROP_INTERVAL_MINUTES] =
		g_param_spec_uint (
			"interval-minutes",
			NULL, NULL,
			0, G_MAXUINT, 60,
			G_PARAM_READWRITE |
			G_PARAM_CONSTRUCT |
			G_PARAM_EXPLICIT_NOTIFY |
			G_PARAM_STATIC_STRINGS |
			E_SOURCE_PARAM_SETTING);

	g_object_class_install_properties (object_class, N_PROPS, properties);
}

static void
e_source_refresh_init (ESourceRefresh *extension)
{
	GHashTable *timeout_table;

	timeout_table = g_hash_table_new_full (
		(GHashFunc) g_direct_hash,
		(GEqualFunc) g_direct_equal,
		(GDestroyNotify) NULL,
		(GDestroyNotify) timeout_node_free);

	extension->priv = e_source_refresh_get_instance_private (extension);
	g_mutex_init (&extension->priv->timeout_lock);
	extension->priv->timeout_table = timeout_table;
	extension->priv->next_timeout_id = 1;
}

/**
 * e_source_refresh_get_enabled:
 * @extension: an #ESourceRefresh
 *
 * Returns whether to periodically fetch updates from a remote server.
 *
 * The refresh interval is determined by the #ESourceRefresh:interval-minutes
 * property.
 *
 * Returns: whether periodic refresh is enabled
 *
 * Since: 3.6
 **/
gboolean
e_source_refresh_get_enabled (ESourceRefresh *extension)
{
	g_return_val_if_fail (E_IS_SOURCE_REFRESH (extension), FALSE);

	return extension->priv->enabled;
}

/**
 * e_source_refresh_set_enabled:
 * @extension: an #ESourceRefresh
 * @enabled: whether to enable periodic refresh
 *
 * Sets whether to periodically fetch updates from a remote server.
 *
 * The refresh interval is determined by the #ESourceRefresh:interval-minutes
 * property.
 *
 * Since: 3.6
 **/
void
e_source_refresh_set_enabled (ESourceRefresh *extension,
                              gboolean enabled)
{
	g_return_if_fail (E_IS_SOURCE_REFRESH (extension));

	extension->priv->enabled = enabled;

	g_object_notify_by_pspec (G_OBJECT (extension), properties[PROP_ENABLED]);

	source_refresh_update_timeouts (extension, FALSE);
}

/**
 * e_source_refresh_get_enabled_on_metered_network:
 * @extension: an #ESourceRefresh
 *
 * Returns whether can refresh content on metered network.
 *
 * The @extension itself doesn't use this option, it's up to
 * the @extension user to determine what kind of connection is used
 * and then decide whether refresh, or other expensive network
 * operations, can be done.
 *
 * Returns: whether can refresh content on metered network
 *
 * Since: 3.50
 **/
gboolean
e_source_refresh_get_enabled_on_metered_network (ESourceRefresh *extension)
{
	g_return_val_if_fail (E_IS_SOURCE_REFRESH (extension), FALSE);

	return extension->priv->enabled_on_metered_network;
}

/**
 * e_source_refresh_set_enabled_on_metered_network:
 * @extension: an #ESourceRefresh
 * @enabled: whether can refresh content on metered network
 *
 * Sets whether can refresh content on metered network.
 *
 * See e_source_refresh_get_enabled_on_metered_network() for more information
 * about what it means.
 *
 * Since: 3.50
 **/
void
e_source_refresh_set_enabled_on_metered_network (ESourceRefresh *extension,
						 gboolean enabled)
{
	g_return_if_fail (E_IS_SOURCE_REFRESH (extension));

	if ((extension->priv->enabled_on_metered_network ? 1 : 0) == (enabled ? 1 : 0))
		return;

	extension->priv->enabled_on_metered_network = enabled;

	g_object_notify_by_pspec (G_OBJECT (extension), properties[PROP_ENABLED_ON_METERED_NETWORK]);
}

/**
 * e_source_refresh_get_interval_minutes:
 * @extension: an #ESourceRefresh
 *
 * Returns the interval for fetching updates from a remote server.
 *
 * Note this value is only effective when the #ESourceRefresh:enabled
 * property is %TRUE.
 *
 * Returns: the interval in minutes
 *
 * Since: 3.6
 **/
guint
e_source_refresh_get_interval_minutes (ESourceRefresh *extension)
{
	g_return_val_if_fail (E_IS_SOURCE_REFRESH (extension), FALSE);

	return extension->priv->interval_minutes;
}

/**
 * e_source_refresh_set_interval_minutes:
 * @extension: an #ESourceRefresh
 * @interval_minutes: the interval in minutes
 *
 * Sets the interval for fetching updates from a remote server.
 *
 * Note this value is only effective when the #ESourceRefresh:enabled
 * property is %TRUE.
 *
 * Since: 3.6
 **/
void
e_source_refresh_set_interval_minutes (ESourceRefresh *extension,
                                       guint interval_minutes)
{
	g_return_if_fail (E_IS_SOURCE_REFRESH (extension));

	if (interval_minutes == extension->priv->interval_minutes)
		return;

	extension->priv->interval_minutes = interval_minutes;

	g_object_notify_by_pspec (G_OBJECT (extension), properties[PROP_INTERVAL_MINUTES]);

	source_refresh_update_timeouts (extension, FALSE);
}

/**
 * e_source_refresh_add_timeout:
 * @source: an #ESource
 * @context: (nullable): a #GMainContext, or %NULL (if %NULL, the default
 *           context will be used)
 * @callback: function to call on each timeout
 * @user_data: data to pass to @callback
 * @notify: (nullable): function to call when the timeout is removed,
 *          or %NULL
 *
 * This is a simple way to schedule a periodic data source refresh.
 *
 * Adds a timeout #GSource to @context and handles all the bookkeeping
 * if @source's refresh #ESourceRefresh:enabled state or its refresh
 * #ESourceRefresh:interval-minutes value changes.  The @callback is
 * expected to dispatch an asynchronous job to connect to and fetch
 * updates from a remote server.
 *
 * The returned ID can be passed to e_source_refresh_remove_timeout() to
 * remove the timeout from @context.  Note the ID is a private handle and
 * cannot be passed to g_source_remove().
 *
 * Returns: a refresh timeout ID
 *
 * Since: 3.6
 **/
guint
e_source_refresh_add_timeout (ESource *source,
                              GMainContext *context,
                              ESourceRefreshFunc callback,
                              gpointer user_data,
                              GDestroyNotify notify)
{
	ESourceRefresh *extension;
	const gchar *extension_name;
	TimeoutNode *node;
	guint timeout_id;
	gpointer key;

	g_return_val_if_fail (E_IS_SOURCE (source), 0);
	g_return_val_if_fail (callback != NULL, 0);

	extension_name = E_SOURCE_EXTENSION_REFRESH;
	extension = e_source_get_extension (source, extension_name);

	g_mutex_lock (&extension->priv->timeout_lock);

	timeout_id = extension->priv->next_timeout_id++;

	key = GUINT_TO_POINTER (timeout_id);
	node = timeout_node_new (
		extension, context, callback, user_data, notify);
	g_hash_table_insert (extension->priv->timeout_table, key, node);

	if (e_source_refresh_get_enabled (extension))
		timeout_node_attach (node);

	g_mutex_unlock (&extension->priv->timeout_lock);

	return timeout_id;
}

/**
 * e_source_refresh_force_timeout:
 * @source: an #ESource
 *
 * For all timeouts added with e_source_refresh_add_timeout(), invokes
 * the #ESourceRefreshFunc callback immediately and then, if the refresh
 * #ESourceRefresh:enabled state is TRUE, reschedules the timeout.
 *
 * This function is called automatically when the #ESource switches from
 * disabled to enabled, but can also be useful when a network connection
 * becomes available or when waking up from hibernation or suspend.
 *
 * Since: 3.6
 **/
void
e_source_refresh_force_timeout (ESource *source)
{
	ESourceRefresh *extension;
	const gchar *extension_name;

	g_return_if_fail (E_IS_SOURCE (source));

	extension_name = E_SOURCE_EXTENSION_REFRESH;
	extension = e_source_get_extension (source, extension_name);

	source_refresh_update_timeouts (extension, TRUE);
}

/**
 * e_source_refresh_remove_timeout:
 * @source: an #ESource
 * @refresh_timeout_id: a refresh timeout ID
 *
 * Removes a timeout #GSource added by e_source_refresh_add_timeout().
 *
 * Returns: %TRUE if the timeout was found and removed
 *
 * Since: 3.6
 **/
gboolean
e_source_refresh_remove_timeout (ESource *source,
                                 guint refresh_timeout_id)
{
	ESourceRefresh *extension;
	const gchar *extension_name;
	gboolean removed;
	gpointer key;

	g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
	g_return_val_if_fail (refresh_timeout_id > 0, FALSE);

	extension_name = E_SOURCE_EXTENSION_REFRESH;
	extension = e_source_get_extension (source, extension_name);

	g_mutex_lock (&extension->priv->timeout_lock);

	key = GUINT_TO_POINTER (refresh_timeout_id);
	removed = g_hash_table_remove (extension->priv->timeout_table, key);

	g_mutex_unlock (&extension->priv->timeout_lock);

	return removed;
}

/**
 * e_source_refresh_remove_timeouts_by_data:
 * @source: an #ESource
 * @user_data: user data to match against timeout callbacks
 *
 * Removes all timeout #GSource's added by e_source_refresh_add_timeout()
 * whose callback data pointer matches @user_data.
 *
 * Returns: the number of timeouts found and removed
 *
 * Since: 3.6
 **/
guint
e_source_refresh_remove_timeouts_by_data (ESource *source,
                                          gpointer user_data)
{
	ESourceRefresh *extension;
	const gchar *extension_name;
	GQueue trash = G_QUEUE_INIT;
	GHashTableIter iter;
	gpointer key, value;
	guint n_removed = 0;

	g_return_val_if_fail (E_IS_SOURCE (source), 0);

	extension_name = E_SOURCE_EXTENSION_REFRESH;
	extension = e_source_get_extension (source, extension_name);

	g_mutex_lock (&extension->priv->timeout_lock);

	g_hash_table_iter_init (&iter, extension->priv->timeout_table);

	while (g_hash_table_iter_next (&iter, &key, &value)) {
		TimeoutNode *node = value;

		if (node->user_data == user_data)
			g_queue_push_tail (&trash, key);
	}

	while ((key = g_queue_pop_head (&trash)) != NULL)
		if (g_hash_table_remove (extension->priv->timeout_table, key))
			n_removed++;

	g_mutex_unlock (&extension->priv->timeout_lock);

	return n_removed;
}

