/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
/*
 * libnm_glib -- Access network status & information from glib applications
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301 USA.
 *
 * Copyright (C) 2005 - 2008 Red Hat, Inc.
 * Copyright (C) 2005 - 2008 Novell, Inc.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <glib.h>
#include <dbus/dbus.h>
#include <dbus/dbus-glib-lowlevel.h>
#include "NetworkManager.h"
#include "libnm_glib.h"

#define	DBUS_NO_SERVICE_ERROR			"org.freedesktop.DBus.Error.ServiceDoesNotExist"
#define NM_DBUS_SIGNAL_STATE_CHANGE	"StateChange"


struct libnm_glib_ctx
{
	unsigned char		check;

	GMainContext *		g_main_ctx;
	GMainLoop *		g_main_loop;
	DBusConnection	*	dbus_con;
	guint			dbus_watcher;
	guint			dbus_watch_interval;

	gboolean			thread_done;
	gboolean			thread_inited;
	GThread *			thread;

	GSList *			callbacks;
	GMutex *			callbacks_lock;
	guint			callback_id_last;

	libnm_glib_state	nm_state;
};

typedef struct libnm_glib_callback
{
	guint				id;
	GMainContext *			gmain_ctx;
	libnm_glib_ctx *		libnm_glib_ctx;
	libnm_glib_callback_func	func;
	gpointer				user_data;
} libnm_glib_callback;


static void libnm_glib_schedule_dbus_watcher (libnm_glib_ctx *ctx);
static DBusConnection * libnm_glib_dbus_init (gpointer *user_data, GMainContext *context);
static void libnm_glib_update_state (libnm_glib_ctx *ctx, NMState state);


static void
libnm_glib_nm_state_cb (DBusPendingCall *pcall,
                        void *user_data)
{
	DBusMessage *		reply;
	libnm_glib_ctx *	ctx = (libnm_glib_ctx *) user_data;
	NMState			nm_state;

	g_return_if_fail (pcall != NULL);
	g_return_if_fail (ctx != NULL);

	if (!(reply = dbus_pending_call_steal_reply (pcall)))
		goto out;

	if (dbus_message_get_type (reply) == DBUS_MESSAGE_TYPE_ERROR)
	{
		DBusError err;

		dbus_error_init (&err);
		dbus_set_error_from_message (&err, reply);
		fprintf (stderr, "%s: dbus returned an error.\n  (%s) %s\n", __func__, err.name, err.message);
		dbus_error_free (&err);
		dbus_message_unref (reply);
		goto out;
	}

	if (dbus_message_get_args (reply, NULL, DBUS_TYPE_UINT32, &nm_state, DBUS_TYPE_INVALID))
		libnm_glib_update_state (ctx, nm_state);

	dbus_message_unref (reply);

out:
	dbus_pending_call_unref (pcall);
}

static void
libnm_glib_get_nm_state (libnm_glib_ctx *ctx)
{
	DBusMessage *		message;
	DBusPendingCall *	pcall = NULL;

	g_return_if_fail (ctx != NULL);

	if ((message = dbus_message_new_method_call (NM_DBUS_SERVICE, NM_DBUS_PATH, NM_DBUS_INTERFACE, "state")))
	{
		dbus_connection_send_with_reply (ctx->dbus_con, message, &pcall, -1);
		if (pcall)
			dbus_pending_call_set_notify (pcall, libnm_glib_nm_state_cb, ctx, NULL);
		dbus_message_unref (message);
	}
}

static gboolean
libnm_glib_callback_helper (gpointer user_data)
{
	libnm_glib_callback	*cb_data = (libnm_glib_callback *)user_data;

	g_return_val_if_fail (cb_data != NULL, FALSE);
	g_return_val_if_fail (cb_data->func != NULL, FALSE);
	g_return_val_if_fail (cb_data->libnm_glib_ctx != NULL, FALSE);

	(*(cb_data->func)) (cb_data->libnm_glib_ctx, cb_data->user_data);

	return FALSE;
}

static void
libnm_glib_schedule_single_callback (libnm_glib_ctx *ctx,
                                     libnm_glib_callback *callback)
{
	GSource				*source;

	g_return_if_fail (ctx != NULL);
	g_return_if_fail (callback != NULL);

	callback->libnm_glib_ctx = ctx;

	source = g_idle_source_new ();
	g_source_set_callback (source, libnm_glib_callback_helper, callback, NULL);
	g_source_attach (source, callback->gmain_ctx);
	g_source_unref (source);
}

static void
libnm_glib_unschedule_single_callback (libnm_glib_ctx *ctx,
                                       libnm_glib_callback *callback)
{
	GSource *source;

	g_return_if_fail (ctx != NULL);
	g_return_if_fail (callback != NULL);

	source = g_main_context_find_source_by_user_data (callback->gmain_ctx, callback);
	if (source)
		g_source_destroy (source);
}

static void
libnm_glib_call_callbacks (libnm_glib_ctx *ctx)
{
	GSList	*elem;

	g_return_if_fail (ctx != NULL);

	g_mutex_lock (ctx->callbacks_lock);
	for (elem = ctx->callbacks; elem; elem = g_slist_next (elem))
	{
		libnm_glib_callback *callback = (libnm_glib_callback *)(elem->data);
		if (callback)
			libnm_glib_schedule_single_callback (ctx, callback);
	}
	g_mutex_unlock (ctx->callbacks_lock);
}


static void
libnm_glib_update_state (libnm_glib_ctx *ctx,
                         NMState state)
{
	libnm_glib_state	old_state;

	g_return_if_fail (ctx != NULL);

	old_state = ctx->nm_state;
	switch (state)
	{
		case NM_STATE_CONNECTED:
			ctx->nm_state = LIBNM_ACTIVE_NETWORK_CONNECTION;
			break;

		case NM_STATE_ASLEEP:
		case NM_STATE_CONNECTING:
		case NM_STATE_DISCONNECTED:
			ctx->nm_state = LIBNM_NO_NETWORK_CONNECTION;
			break;

		case NM_STATE_UNKNOWN:
		default:
			ctx->nm_state = LIBNM_NO_NETWORKMANAGER;
			break;
	}

	if (old_state != ctx->nm_state)
		libnm_glib_call_callbacks (ctx);
}


static DBusHandlerResult
libnm_glib_dbus_filter (DBusConnection *connection,
                        DBusMessage *message,
                        void *user_data)
{
	libnm_glib_ctx		*ctx = (libnm_glib_ctx *)user_data;
	gboolean		 handled = TRUE;
	DBusError	 	 error;

	g_return_val_if_fail (ctx != NULL, DBUS_HANDLER_RESULT_NOT_YET_HANDLED);
	g_return_val_if_fail (connection != NULL, DBUS_HANDLER_RESULT_NOT_YET_HANDLED);
	g_return_val_if_fail (message != NULL, DBUS_HANDLER_RESULT_NOT_YET_HANDLED);

	dbus_error_init (&error);
	if (dbus_message_is_signal (message, DBUS_INTERFACE_LOCAL, "Disconnected"))
	{
		/* Try to reactivate our connection to dbus on the next pass through the event loop */
		ctx->nm_state = LIBNM_NO_DBUS;
		dbus_connection_close (ctx->dbus_con);
		dbus_connection_unref (ctx->dbus_con);
		ctx->dbus_con = NULL;
		libnm_glib_schedule_dbus_watcher (ctx);
	}
	else if (dbus_message_is_signal (message, DBUS_INTERFACE_DBUS, "NameOwnerChanged"))
	{
		/* New signal for dbus 0.23... */
		char 	*service;
		char		*old_owner;
		char		*new_owner;

		if (    dbus_message_get_args (message, &error,
									DBUS_TYPE_STRING, &service,
									DBUS_TYPE_STRING, &old_owner,
									DBUS_TYPE_STRING, &new_owner,
									DBUS_TYPE_INVALID))
		{
			if (strcmp (service, NM_DBUS_SERVICE) == 0)
			{
				gboolean old_owner_good = (old_owner && (strlen (old_owner) > 0));
				gboolean new_owner_good = (new_owner && (strlen (new_owner) > 0));

				if (!old_owner_good && new_owner_good)	/* Equivalent to old ServiceCreated signal */
					libnm_glib_get_nm_state (ctx);
				else if (old_owner_good && !new_owner_good)	/* Equivalent to old ServiceDeleted signal */
					ctx->nm_state = LIBNM_NO_NETWORKMANAGER;
			}
		}
	}
	else if (    dbus_message_is_signal (message, NM_DBUS_INTERFACE, "DeviceNowActive")
			|| dbus_message_is_signal (message, NM_DBUS_INTERFACE, "DeviceNoLongerActive")
			|| dbus_message_is_signal (message, NM_DBUS_INTERFACE, "DeviceActivating")
			|| dbus_message_is_signal (message, NM_DBUS_INTERFACE, "DevicesChanged"))
	{
		libnm_glib_get_nm_state (ctx);
	}
	else if (dbus_message_is_signal (message, NM_DBUS_INTERFACE, NM_DBUS_SIGNAL_STATE_CHANGE))
	{
		NMState	state = NM_STATE_UNKNOWN;

		dbus_message_get_args (message, &error, DBUS_TYPE_UINT32, &state, DBUS_TYPE_INVALID);
		libnm_glib_update_state (ctx, state);
	}
	else
		handled = FALSE;

	if (dbus_error_is_set (&error))
		dbus_error_free (&error);

	return (handled ? DBUS_HANDLER_RESULT_HANDLED : DBUS_HANDLER_RESULT_NOT_YET_HANDLED);
}


/*
 * libnm_glib_dbus_init
 *
 * Initialize a connection to dbus and set up our callbacks.
 *
 */
static DBusConnection *
libnm_glib_dbus_init (gpointer *user_data,
                      GMainContext *context)
{
	DBusConnection	*connection = NULL;
	DBusError		 error;

	dbus_error_init (&error);
	connection = dbus_bus_get_private (DBUS_BUS_SYSTEM, &error);
	if (dbus_error_is_set (&error))
	{
		fprintf (stderr, "%s: error, %s raised:\n %s\n\n", __func__, error.name, error.message);
		dbus_error_free (&error);
		return (NULL);
	}
	if (!connection)
		return NULL;

	if (!dbus_connection_add_filter (connection, libnm_glib_dbus_filter, user_data, NULL))
		return (NULL);

	dbus_connection_set_exit_on_disconnect (connection, FALSE);
	dbus_connection_setup_with_g_main (connection, context);

	dbus_error_init (&error);
	dbus_bus_add_match (connection,
				"type='signal',"
				"interface='" DBUS_INTERFACE_DBUS "',"
				"sender='" DBUS_SERVICE_DBUS "'",
				&error);
	if (dbus_error_is_set (&error))
		dbus_error_free (&error);

	dbus_error_init (&error);
	dbus_bus_add_match (connection,
				"type='signal',"
				"interface='" NM_DBUS_INTERFACE "',"
				"path='" NM_DBUS_PATH "',"
				"sender='" NM_DBUS_SERVICE "'",
				&error);
	if (dbus_error_is_set (&error))
		dbus_error_free (&error);

	return (connection);
}


/*
 * libnm_glib_dbus_watcher
 *
 * Repeatedly try to re-activate the connection to dbus.
 *
 */
static gboolean
libnm_glib_dbus_watcher (gpointer user_data)
{
	libnm_glib_ctx	*ctx = (libnm_glib_ctx *)user_data;

	g_return_val_if_fail (ctx != NULL, FALSE);

	ctx->dbus_watcher = 0;

	if (!ctx->dbus_con)
		ctx->dbus_con = libnm_glib_dbus_init ((gpointer)ctx, ctx->g_main_ctx);

	if (ctx->dbus_con)
	{
		/* Get NM's state right away after we reconnect */
		libnm_glib_get_nm_state (ctx);
		ctx->dbus_watch_interval = 1000;
	}
	else
	{
		/* Wait 3 seconds longer each time we fail to reconnect to dbus,
		 * with a maximum wait of one minute.
		 */
		ctx->dbus_watch_interval = MIN(ctx->dbus_watch_interval + 3000, 60000); 

		/* Reschule ourselves if we _still_ don't have a connection to dbus */
		libnm_glib_schedule_dbus_watcher (ctx);
	}

	return FALSE;
}


/*
 * libnm_glib_schedule_dbus_watcher
 *
 * Schedule an idle handler in our main loop to repeatedly
 * attempt to re-activate the dbus connection until connected.
 *
 */
static void
libnm_glib_schedule_dbus_watcher (libnm_glib_ctx *ctx)
{
	g_return_if_fail (ctx != NULL);

	if (ctx->dbus_watcher == 0)
	{
		GSource *	source = g_timeout_source_new (ctx->dbus_watch_interval);
		g_source_set_callback (source, libnm_glib_dbus_watcher, (gpointer) ctx, NULL);
		ctx->dbus_watcher = g_source_attach (source, ctx->g_main_ctx);
		g_source_unref (source);
	}
}


/*
 * libnm_glib_dbus_worker
 *
 * Main thread for libnm
 *
 */
static gpointer
libnm_glib_dbus_worker (gpointer user_data)
{
	libnm_glib_ctx	*ctx = (libnm_glib_ctx *)user_data;

	g_return_val_if_fail (ctx != NULL, NULL);

	/* If dbus isn't up yet, schedule an idle handler to check for dbus.
	 * We also need a way to reconnect to dbus if the connection ever goes
	 * down.  Should probably be done by a timeout polling dbus_connection_is_connected()
	 * or by getting connection status out of libdbus or something.
	 */
	if (!(ctx->dbus_con = libnm_glib_dbus_init ((gpointer) ctx, ctx->g_main_ctx)))
		libnm_glib_schedule_dbus_watcher (ctx);
	else
		libnm_glib_get_nm_state (ctx);

	ctx->thread_inited = TRUE;
	g_main_loop_run (ctx->g_main_loop);
	ctx->thread_done = TRUE;

	return NULL;
}


static void
libnm_glib_ctx_free (libnm_glib_ctx *ctx)
{
	g_return_if_fail (ctx != NULL);

	if (ctx->check == 0xDD)
	{
		fprintf (stderr, "%s: context %p already freed!\n", __func__, ctx);
		return;
	}

	if (ctx->g_main_ctx)
		g_main_context_unref (ctx->g_main_ctx);
	if (ctx->g_main_loop)
		g_main_loop_unref (ctx->g_main_loop);

	if (ctx->dbus_con)
	{
		dbus_connection_close (ctx->dbus_con);
		dbus_connection_unref (ctx->dbus_con);
		ctx->dbus_con = NULL;
	}

	if (ctx->callbacks_lock)
		g_mutex_free (ctx->callbacks_lock);

	g_slist_foreach (ctx->callbacks, (GFunc)g_free, NULL);
	g_slist_free (ctx->callbacks);

	if (ctx->thread)
		g_thread_join (ctx->thread);

	memset (ctx, 0, sizeof (libnm_glib_ctx));
	memset (&(ctx->check), 0xDD, sizeof (ctx->check));
	g_free (ctx);
}


static libnm_glib_ctx *
libnm_glib_ctx_new (void)
{
	libnm_glib_ctx *ctx = g_malloc0 (sizeof (libnm_glib_ctx));

	if (!(ctx->g_main_ctx = g_main_context_new ()))
		goto error;
	if (!(ctx->g_main_loop = g_main_loop_new (ctx->g_main_ctx, FALSE)))
		goto error;
	if (!(ctx->callbacks_lock = g_mutex_new ()))
		goto error;
	ctx->dbus_watch_interval = 1000;

	return ctx;

error:
	libnm_glib_ctx_free (ctx);
	return NULL;
}


libnm_glib_ctx *
libnm_glib_init (void)
{
	libnm_glib_ctx	*ctx = NULL;

	g_type_init ();
	if (!g_thread_supported ())
		g_thread_init (NULL);
	dbus_g_thread_init ();

	if (!(ctx = libnm_glib_ctx_new ()))
		return NULL;

	ctx->thread = g_thread_create (libnm_glib_dbus_worker, ctx, TRUE, NULL);
	if (!ctx->thread)
		goto error;	

	/* Wait until initialization of the thread */
	while (!ctx->thread_inited)
		g_usleep (G_USEC_PER_SEC / 20);

	return ctx;

error:
	libnm_glib_ctx_free (ctx);
	return NULL;
}


void
libnm_glib_shutdown (libnm_glib_ctx *ctx)
{
	g_return_if_fail (ctx != NULL);

	g_main_loop_quit (ctx->g_main_loop);
	while (!ctx->thread_done)
		g_usleep (G_USEC_PER_SEC / 20);

	libnm_glib_ctx_free (ctx);
}


libnm_glib_state
libnm_glib_get_network_state (const libnm_glib_ctx *ctx)
{
	if (!ctx)
		return LIBNM_INVALID_CONTEXT;

	return ctx->nm_state;
}


guint
libnm_glib_register_callback	(libnm_glib_ctx *ctx,
                               libnm_glib_callback_func func,
                               gpointer user_data,
                               GMainContext *g_main_ctx)
{
	libnm_glib_callback		*callback = NULL;

	g_return_val_if_fail (ctx != NULL, 0);
	g_return_val_if_fail (func != NULL, 0);

	callback = g_malloc0 (sizeof (libnm_glib_callback));

	callback->id = ++ (ctx->callback_id_last);
	callback->func = func;
	callback->gmain_ctx = g_main_ctx;
	callback->libnm_glib_ctx = ctx;
	callback->user_data = user_data;

	g_mutex_lock (ctx->callbacks_lock);
	ctx->callbacks = g_slist_append (ctx->callbacks, callback);
	libnm_glib_schedule_single_callback (ctx, callback);
	g_mutex_unlock (ctx->callbacks_lock);

	return (callback->id);
}


void
libnm_glib_unregister_callback (libnm_glib_ctx *ctx,
                                guint id)
{
	GSList *elem;

	g_return_if_fail (ctx != NULL);
	g_return_if_fail (id > 0);

	g_mutex_lock (ctx->callbacks_lock);
	elem = ctx->callbacks;
	while (elem)
	{
		libnm_glib_callback *callback = (libnm_glib_callback *)(elem->data);
		if (callback && (callback->id == id))
		{
			libnm_glib_unschedule_single_callback (ctx, callback);
			ctx->callbacks = g_slist_remove_link (ctx->callbacks, elem);
			break;
		}

		elem = g_slist_next (elem);
	}
	g_mutex_unlock (ctx->callbacks_lock);
}
