/*
 *  oFono - Open Source Telephony - RIL-based devices
 *
 *  Copyright (C) 2019-2021 Jolla Ltd.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program 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 General Public License for more details.
 */

#include "ril_connman.h"

#include <ofono/log.h>
#include <ofono/gdbus.h>

#include <gutil_macros.h>
#include <gutil_misc.h>

#include <glib-object.h>

#define CONNMAN_BUS DBUS_BUS_SYSTEM
#define CONNMAN_SERVICE "net.connman"
#define CONNMAN_PATH "/"

#define CONNMAN_GET_PROPERTIES "GetProperties"
#define CONNMAN_GET_TECHNOLOGIES "GetTechnologies"
#define CONNMAN_PROPERTY_CHANGED "PropertyChanged"
#define CONNMAN_TECH_CONNECTED "Connected"
#define CONNMAN_TECH_TETHERING "Tethering"

#define CONNMAN_INTERFACE_(name) "net.connman." name
#define CONNMAN_MANAGER_INTERFACE CONNMAN_INTERFACE_("Manager")
#define CONNMAN_TECH_INTERFACE CONNMAN_INTERFACE_("Technology")

#define CONNMAN_TECH_PATH_(name) "/net/connman/technology/" name
#define CONNMAN_TECH_PATH_WIFI CONNMAN_TECH_PATH_("wifi")

#define CONNMAN_TECH_CONNECTED_BIT (0x01)
#define CONNMAN_TECH_TETHERING_BIT (0x02)
#define CONNMAN_TECH_ALL_PROPERTY_BITS (\
	CONNMAN_TECH_CONNECTED_BIT | \
	CONNMAN_TECH_TETHERING_BIT)

typedef GObjectClass ConnManObjectClass;

typedef struct connman_tech ConnManTech;

typedef struct connman_object {
	GObject object;
	struct ril_connman pub;
	guint32 pending_signals;
	DBusConnection *connection;
	DBusPendingCall *call;
	guint service_watch;
	guint signal_watch;
	GHashTable *techs;
	ConnManTech *wifi;
} ConnManObject;

G_DEFINE_TYPE(ConnManObject, connman_object, G_TYPE_OBJECT)
#define CONNMAN_OBJECT_TYPE (connman_object_get_type())
#define CONNMAN_OBJECT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),\
	CONNMAN_OBJECT_TYPE, ConnManObject))

struct connman_tech {
	ConnManObject *obj;
	const char *path;
	gboolean connected;
	gboolean tethering;
};

typedef struct connman_closure {
	GCClosure cclosure;
	ril_connman_property_cb_t callback;
	gpointer user_data;
} ConnManClosure;

#define connman_closure_new() ((ConnManClosure *) \
    g_closure_new_simple(sizeof(ConnManClosure), NULL))

#define SIGNAL_PROPERTY_CHANGED_NAME    "ril-connman-property-changed"
#define SIGNAL_PROPERTY_DETAIL          "%x"
#define SIGNAL_PROPERTY_DETAIL_MAX_LEN  (8)

#define SIGNAL_BIT(property) (1 << (property - 1))
#define SIGNAL_BIT_(name) SIGNAL_BIT(RIL_CONNMAN_PROPERTY_##name)

enum connman_object_signal {
	SIGNAL_PROPERTY_CHANGED,
	SIGNAL_COUNT
};

static guint connman_object_signals[SIGNAL_COUNT];
static GQuark connman_object_property_quarks[RIL_CONNMAN_PROPERTY_COUNT - 1];

static inline ConnManObject *connman_object_cast(struct ril_connman *connman)
{
	return G_LIKELY(connman) ?
		CONNMAN_OBJECT(G_CAST(connman, ConnManObject, pub)) :
		NULL;
}

static inline const char *connman_iter_get_string(DBusMessageIter *it)
{
	const char *str = NULL;

	dbus_message_iter_get_basic(it, &str);
	return str;
}

static GQuark connman_object_property_quark(enum ril_connman_property p)
{
	/* For ANY property this function is expected to return zero */
	if (p > RIL_CONNMAN_PROPERTY_ANY && p < RIL_CONNMAN_PROPERTY_COUNT) {
		const int i = p - 1;

		if (G_UNLIKELY(!connman_object_property_quarks[i])) {
			char buf[SIGNAL_PROPERTY_DETAIL_MAX_LEN + 1];

			snprintf(buf, sizeof(buf), SIGNAL_PROPERTY_DETAIL, p);
			buf[sizeof(buf) - 1] = 0;
			connman_object_property_quarks[i] =
				g_quark_from_string(buf);
		}
		return connman_object_property_quarks[i];
	}
	return 0;
}

static void connman_object_property_changed(ConnManObject *self,
		enum ril_connman_property property, ConnManClosure *closure)
{
	closure->callback(&self->pub, property, closure->user_data);
}

static void connman_object_emit_property_change(ConnManObject *self,
						enum ril_connman_property p)
{
	self->pending_signals &= ~SIGNAL_BIT(p);
	g_signal_emit(self, connman_object_signals[SIGNAL_PROPERTY_CHANGED],
		connman_object_property_quark(p), p);
}

static void connman_object_emit_pending_signals(ConnManObject *self)
{
	struct ril_connman *connman = &self->pub;
	gboolean valid_changed, present_changed;
	enum ril_connman_property p;

	/* Handlers could drop their references to us */
	g_object_ref(self);

	/*
	 * PRESENT and VALID are the last signals to be emitted if the object
	 * BECOMES present and/or valid.
	 */
	if ((self->pending_signals & SIGNAL_BIT_(VALID)) &&
						connman->valid) {
		self->pending_signals &= ~SIGNAL_BIT_(VALID);
		valid_changed = TRUE;
	} else {
		valid_changed = FALSE;
	}
	if ((self->pending_signals & SIGNAL_BIT_(PRESENT)) &&
						connman->present) {
		self->pending_signals &= ~SIGNAL_BIT_(PRESENT);
		present_changed = TRUE;
	} else {
		present_changed = FALSE;
	}

	/*
	 * Emit the signals. Not that in case if valid has become FALSE,
	 * then VALID is emitted first, otherwise it's emitted last.
	 * Same thing with PRESENT.
	 */
	for (p = RIL_CONNMAN_PROPERTY_ANY + 1;
		p < RIL_CONNMAN_PROPERTY_COUNT && self->pending_signals;
		p++) {
		if (self->pending_signals & SIGNAL_BIT(p)) {
			connman_object_emit_property_change(self, p);
		}
	}

	/* Then emit PRESENT and VALID if necessary */
	if (present_changed) {
		connman_object_emit_property_change(self,
			RIL_CONNMAN_PROPERTY_PRESENT);
	}
	if (valid_changed) {
		connman_object_emit_property_change(self,
			RIL_CONNMAN_PROPERTY_VALID);
	}

	/* And release the temporary reference */
	g_object_unref(self);
}

static void connman_cancel_call(ConnManObject *self)
{
	if (self->call) {
		dbus_pending_call_cancel(self->call);
		dbus_pending_call_unref(self->call);
		self->call = NULL;
	}
}

static ConnManTech *connman_tech_new(ConnManObject *self, const char *path)
{
	ConnManTech *tech = g_new0(ConnManTech, 1);
	char *key = g_strdup(path);

	tech->obj = self;
	tech->path = key;
	g_hash_table_replace(self->techs, key, tech);
	return tech;
}

static void connman_invalidate(ConnManObject *self)
{
	struct ril_connman *connman = &self->pub;

	if (connman->valid) {
		connman->valid = FALSE;
		self->pending_signals |= SIGNAL_BIT_(VALID);
	}
}

static void connman_update_valid(ConnManObject *self)
{
	struct ril_connman *connman = &self->pub;
	const gboolean valid = (connman->present && !self->call);

	if (connman->valid != valid) {
		connman->valid = valid;
		self->pending_signals |= SIGNAL_BIT_(VALID);
	}
}

static gboolean connman_update_tethering(ConnManObject *self)
{
	struct ril_connman *connman = &self->pub;
	gboolean tethering = FALSE;
	GHashTableIter it;
	gpointer value;

	g_hash_table_iter_init(&it, self->techs);
	while (g_hash_table_iter_next(&it, NULL, &value)) {
		const ConnManTech *tech = value;

		if (tech->tethering) {
			tethering = TRUE;
			break;
		}
	}

	if (connman->tethering != tethering) {
		connman->tethering = tethering;
		self->pending_signals |= SIGNAL_BIT_(TETHERING);
		return TRUE;
	} else {
		return FALSE;
	}
}

static void connman_set_tech_tethering(ConnManTech *tech, gboolean tethering)
{
	if (tech->tethering != tethering) {
		ConnManObject *self = tech->obj;

		tech->tethering = tethering;
		DBG(CONNMAN_TECH_TETHERING " %s for %s",
				tethering ? "on" : "off", tech->path);
		if (tethering) {
			struct ril_connman *connman = &self->pub;

			if (G_LIKELY(!connman->tethering)) {
				/* Definitely tethering now */
				connman->tethering = TRUE;
				self->pending_signals |= SIGNAL_BIT_(TETHERING);
				DBG("Tethering on");
			}
		} else if (connman_update_tethering(self)) {
			/* Not tethering anymore */
			DBG("Tethering off");
		}
	}
}

static void connman_set_tech_connected(ConnManTech *tech, gboolean connected)
{
	if (tech->connected != connected) {
		ConnManObject *self = tech->obj;

		tech->connected = connected;
		DBG(CONNMAN_TECH_CONNECTED " %s for %s",
				connected ? "on" : "off", tech->path);
		if (tech == self->wifi) {
			struct ril_connman *connman = &self->pub;

			connman->wifi_connected = connected;
			self->pending_signals |= SIGNAL_BIT_(WIFI_CONNECTED);
			DBG("WiFi %sconnected", connected ? "" : "dis");
		}
	}
}

static int connman_tech_set_property(ConnManTech *tech, DBusMessageIter *it)
{
	DBusMessageIter var;
	DBusBasicValue value;
	const char *key = connman_iter_get_string(it);

	dbus_message_iter_next(it);
	dbus_message_iter_recurse(it, &var);
	dbus_message_iter_get_basic(&var, &value);
	if (!g_ascii_strcasecmp(key, CONNMAN_TECH_CONNECTED)) {
		if (dbus_message_iter_get_arg_type(&var) == DBUS_TYPE_BOOLEAN) {
			connman_set_tech_connected(tech, value.bool_val);
			return CONNMAN_TECH_CONNECTED_BIT;
		}
	} else if (!g_ascii_strcasecmp(key, CONNMAN_TECH_TETHERING)) {
		if (dbus_message_iter_get_arg_type(&var) == DBUS_TYPE_BOOLEAN) {
			connman_set_tech_tethering(tech, value.bool_val);
			return CONNMAN_TECH_TETHERING_BIT;
		}
	}
	return 0;
}

static void connman_tech_set_properties(ConnManTech *tech, DBusMessageIter *it)
{
	DBusMessageIter dict;
	int handled = 0;

	dbus_message_iter_recurse(it, &dict);
	while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {
		DBusMessageIter entry;

		dbus_message_iter_recurse(&dict, &entry);
		handled |= connman_tech_set_property(tech, &entry);
		if (handled == CONNMAN_TECH_ALL_PROPERTY_BITS) {
			/* Ignore the rest */
			break;
		}
		dbus_message_iter_next(&dict);
	}
}

static gboolean connman_tech_property_changed(DBusConnection *conn,
					DBusMessage *msg, void *user_data)
{
	const char *path = dbus_message_get_path(msg);
	ConnManObject *self = CONNMAN_OBJECT(user_data);
	ConnManTech *tech = g_hash_table_lookup(self->techs, path);
	DBusMessageIter it;

	if (tech && dbus_message_has_signature(msg, "sv") &&
					dbus_message_iter_init(msg, &it)) {
		const char* name = connman_iter_get_string(&it);

		if (!connman_tech_set_property(tech, &it)) {
			DBG("%s changed for %s", name, path);
		}
		connman_object_emit_pending_signals(self);
	}
	return TRUE;
}

static void connman_set_techs(ConnManObject *self, DBusMessageIter *it)
{
	DBusMessageIter list;

	dbus_message_iter_recurse(it, &list);
	while (dbus_message_iter_get_arg_type(&list) == DBUS_TYPE_STRUCT) {
		DBusMessageIter entry;
		const char *path;
		ConnManTech *tech;

		dbus_message_iter_recurse(&list, &entry);
		path = connman_iter_get_string(&entry);
		tech = connman_tech_new(self, path);

		DBG("%s", path);
		if (!g_strcmp0(path, CONNMAN_TECH_PATH_WIFI)) {
			/* WiFi is a special case */
			self->wifi = tech;
		}

		dbus_message_iter_next(&entry);
		connman_tech_set_properties(tech, &entry);
		dbus_message_iter_next(&list);
	}
}

static void connman_techs_reply(DBusPendingCall *call, void *user_data)
{
	ConnManObject *self = CONNMAN_OBJECT(user_data);
	DBusMessage *reply = dbus_pending_call_steal_reply(call);
	DBusError error;
	DBusMessageIter array;

	dbus_error_init(&error);
	if (dbus_set_error_from_message(&error, reply)) {
		DBG("Failed to get technologies: %s", error.message);
		dbus_error_free(&error);
	} else if (dbus_message_has_signature(reply, "a(oa{sv})") &&
				dbus_message_iter_init(reply, &array)) {
		connman_set_techs(self, &array);
	}

	dbus_message_unref(reply);
	dbus_pending_call_unref(self->call);
	self->call = NULL;
	connman_update_valid(self);
	connman_object_emit_pending_signals(self);
}

static void connman_get_techs(ConnManObject *self)
{
	DBusMessage *msg = dbus_message_new_method_call(CONNMAN_SERVICE,
				CONNMAN_PATH, CONNMAN_MANAGER_INTERFACE,
				CONNMAN_GET_TECHNOLOGIES);

	connman_cancel_call(self);
	if (g_dbus_send_message_with_reply(self->connection, msg,
					&self->call, DBUS_TIMEOUT_INFINITE)) {
		/* Not valid while any request is pending */
		connman_invalidate(self);
		dbus_pending_call_set_notify(self->call, connman_techs_reply,
								self, NULL);
	}
	dbus_message_unref(msg);
}

static void connman_appeared(DBusConnection *conn, void *user_data)
{
	ConnManObject *self = CONNMAN_OBJECT(user_data);
	struct ril_connman *connman = &self->pub;

	if (!connman->present) {
		DBG("connman is there");
		connman->present = TRUE;
		self->pending_signals |= SIGNAL_BIT_(PRESENT);
		connman_get_techs(self);
		connman_object_emit_pending_signals(self);
	}
}

static void connman_vanished(DBusConnection *conn, void *user_data)
{
	ConnManObject *self = CONNMAN_OBJECT(user_data);
	struct ril_connman *connman = &self->pub;

	if (connman->present) {

		DBG("connman has disappeared");
		g_hash_table_remove_all(self->techs);
		self->wifi = NULL;
		connman->present = FALSE;
		self->pending_signals |= SIGNAL_BIT_(PRESENT);
		if (connman->wifi_connected) {
			connman->wifi_connected = FALSE;
			self->pending_signals |= SIGNAL_BIT_(WIFI_CONNECTED);
		}
		if (connman->tethering) {
			connman->tethering = FALSE;
			self->pending_signals |= SIGNAL_BIT_(TETHERING);
		}
		connman_object_emit_pending_signals(self);
	}
}

static void connman_init(ConnManObject *self, DBusConnection *connection)
{
	self->connection = dbus_connection_ref(connection);
	self->service_watch = g_dbus_add_service_watch(self->connection,
		CONNMAN_SERVICE, connman_appeared, connman_vanished,
		self, NULL);
	self->signal_watch = g_dbus_add_signal_watch(self->connection,
		CONNMAN_SERVICE, NULL, CONNMAN_TECH_INTERFACE,
		CONNMAN_PROPERTY_CHANGED, connman_tech_property_changed,
		self, NULL);
}

struct ril_connman *ril_connman_new()
{
	static ConnManObject *instance = NULL;

	if (instance) {
		g_object_ref(instance);
		return &instance->pub;
	} else {
		DBusError error;
		DBusConnection *connection;

		dbus_error_init(&error);
		connection = dbus_bus_get(CONNMAN_BUS, NULL);

		if (connection) {
			instance = g_object_new(CONNMAN_OBJECT_TYPE, NULL);
			connman_init(instance, connection);
			dbus_connection_unref(connection);
			g_object_add_weak_pointer(G_OBJECT(instance),
				(gpointer*)(&instance));
			return &instance->pub;
		} else {
			ofono_error("Unable to attach to connman bus: %s",
				error.message);
			dbus_error_free(&error);
			return NULL;
		}
	}
}

struct ril_connman *ril_connman_ref(struct ril_connman *connman)
{
	ConnManObject *self = connman_object_cast(connman);

	if (G_LIKELY(self)) {
		g_object_ref(self);
		return connman;
	} else {
		return NULL;
	}
}

void ril_connman_unref(struct ril_connman *connman)
{
	ConnManObject *self = connman_object_cast(connman);

	if (G_LIKELY(self)) {
		g_object_unref(self);
	}
}

gulong ril_connman_add_property_changed_handler(struct ril_connman *connman,
	enum ril_connman_property p, ril_connman_property_cb_t cb, void *arg)
{
	ConnManObject *self = connman_object_cast(connman);

	if (G_LIKELY(self) && G_LIKELY(cb)) {
		/*
		 * We can't directly connect the provided callback because
		 * it expects the first parameter to point to public part
		 * of the object but glib will call it with ConnManObject
		 * as the first parameter. connman_object_property_changed()
		 * will do the conversion.
		 */
		ConnManClosure *closure = connman_closure_new();
		GCClosure *cc = &closure->cclosure;

		cc->closure.data = closure;
		cc->callback = G_CALLBACK(connman_object_property_changed);
		closure->callback = cb;
		closure->user_data = arg;

		return g_signal_connect_closure_by_id(self,
			connman_object_signals[SIGNAL_PROPERTY_CHANGED],
			connman_object_property_quark(p), &cc->closure, FALSE);
	}
	return 0;
}

void ril_connman_remove_handler(struct ril_connman *connman, gulong id)
{
	if (G_LIKELY(id)) {
		ConnManObject *self = connman_object_cast(connman);

		if (G_LIKELY(self)) {
			g_signal_handler_disconnect(self, id);
		}
	}
}

void ril_connman_remove_handlers(struct ril_connman *connman, gulong *ids,
								int n)
{
	gutil_disconnect_handlers(connman_object_cast(connman), ids, n);
}

static void connman_object_init(ConnManObject *self)
{
	self->techs = g_hash_table_new_full(g_str_hash, g_str_equal,
							g_free, g_free);
}

static void connman_object_finalize(GObject *object)
{
	ConnManObject *self = CONNMAN_OBJECT(object);

	connman_cancel_call(self);
	g_hash_table_destroy(self->techs);
	g_dbus_remove_watch(self->connection, self->service_watch);
	g_dbus_remove_watch(self->connection, self->signal_watch);
	dbus_connection_unref(self->connection);
	G_OBJECT_CLASS(connman_object_parent_class)->finalize(object);
}

static void connman_object_class_init(ConnManObjectClass *klass)
{
	G_OBJECT_CLASS(klass)->finalize = connman_object_finalize;
	connman_object_signals[SIGNAL_PROPERTY_CHANGED] =
		g_signal_new(SIGNAL_PROPERTY_CHANGED_NAME,
			G_OBJECT_CLASS_TYPE(klass),
			G_SIGNAL_RUN_FIRST | G_SIGNAL_DETAILED,
			0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_UINT);
}

/*
 * Local Variables:
 * mode: C
 * c-basic-offset: 8
 * indent-tabs-mode: t
 * End:
 */
