// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (c) 2024, Richard Acayan. All rights reserved.
 */

#include <libmm-glib.h>
#include <stdbool.h>

#include "qvd-connection.h"

enum {
	ADDR_CHANGED,
	CONNECT_ERR,
	DISCONNECT_ERR,
	N_SIGNALS,
};

struct _QVDConnection {
	GObject parent_instance;

	MMBearer *bearer;
	MMBearerIpFamily ip_type;
	gchar *addr;
	gboolean requested;

	guint32 sub_id, conn_id;
	guint8 local_id;
};

G_DEFINE_TYPE(QVDConnection, qvd_connection, G_TYPE_OBJECT)

static guint conn_signals[N_SIGNALS];

static void qvd_connection_dispose(GObject *obj);

static void qvd_connection_class_init(QVDConnectionClass *c)
{
	GObjectClass *obj_class = G_OBJECT_CLASS(c);

	obj_class->dispose = qvd_connection_dispose;

	conn_signals[ADDR_CHANGED] = g_signal_new("qvd-address-changed",
						  QVD_TYPE_CONNECTION,
						  G_SIGNAL_RUN_LAST,
						  0, NULL, NULL, NULL,
						  G_TYPE_NONE,
						  2, G_TYPE_INT, G_TYPE_STRING);
	conn_signals[CONNECT_ERR] = g_signal_new("qvd-connect-error",
						 QVD_TYPE_CONNECTION,
						 G_SIGNAL_RUN_LAST,
						 0, NULL, NULL, NULL,
						 G_TYPE_NONE,
						 1, G_TYPE_ERROR);
	conn_signals[DISCONNECT_ERR] = g_signal_new("qvd-disconnect-error",
						    QVD_TYPE_CONNECTION,
						    G_SIGNAL_RUN_LAST,
						    0, NULL, NULL, NULL,
						    G_TYPE_NONE,
						    1, G_TYPE_ERROR);
}

static void qvd_connection_init(QVDConnection *conn)
{
}

static void qvd_connection_dispose(GObject *obj)
{
	QVDConnection *conn = QVD_CONNECTION(obj);

	if (conn->addr != NULL)
		g_free(g_steal_pointer(&conn->addr));

	g_clear_object(&conn->bearer);
}

QVDConnection *qvd_connection_new(MMBearerIpFamily ip_type)
{
	QVDConnection *conn;

	conn = QVD_CONNECTION(g_object_new(QVD_TYPE_CONNECTION, NULL));
	conn->ip_type = ip_type;

	return conn;
}

const gchar *qvd_connection_get_path(QVDConnection *conn)
{
	if (conn->bearer == NULL)
		return NULL;

	return g_dbus_proxy_get_object_path(G_DBUS_PROXY(conn->bearer));
}

void qvd_connection_get_handles(const QVDConnection *conn,
				guint32 *sub_id, guint32 *conn_id,
				guint8 *local_id)
{
	g_assert(conn != NULL);

	if (sub_id != NULL)
		*sub_id = conn->sub_id;

	if (conn_id != NULL)
		*conn_id = conn->conn_id;

	if (local_id != NULL)
		*local_id = conn->local_id;
}

static void try_notify_address(QVDConnection *conn,
			       GVariant *changed,
			       MMBearerIpFamily ip_type,
			       const gchar *conf_name)
{
	GVariant *ip_conf;
	gchar *addr = NULL;

	ip_conf = g_variant_lookup_value(changed, conf_name, G_VARIANT_TYPE_DICTIONARY);
	if (ip_conf == NULL)
		return;

	g_variant_lookup(ip_conf, "address", "s", &addr);

	if (g_strcmp0(addr, conn->addr)) {
		g_signal_emit(conn, conn_signals[ADDR_CHANGED], 0, ip_type, addr);
		conn->addr = addr;
	} else {
		g_free(addr);
	}
}

void qvd_connection_set_handles(QVDConnection *conn,
				guint32 sub_id, guint32 conn_id,
				guint8 local_id)
{
	g_assert(conn != NULL);

	conn->sub_id = sub_id;
	conn->conn_id = conn_id;
	conn->local_id = local_id;
}

static void on_connect(GObject *obj, GAsyncResult *res, gpointer data)
{
	QVDConnection *conn = QVD_CONNECTION(data);
	GError *err = NULL;

	mm_bearer_connect_finish(MM_BEARER(obj), res, &err);
	if (err != NULL) {
		g_printerr("Failed to connect: %s\n", err->message);
		g_signal_emit(conn, conn_signals[CONNECT_ERR], 0, err);
		g_clear_error(&err);
	}

	g_object_unref(conn);
}

void qvd_connection_connect(QVDConnection *conn)
{
	g_assert(conn != NULL);

	conn->requested = TRUE;

	if (conn->bearer != NULL)
		mm_bearer_connect(conn->bearer, NULL, on_connect, g_object_ref(conn));
}

static void on_disconnect(GObject *obj, GAsyncResult *res, gpointer data)
{
	QVDConnection *conn = QVD_CONNECTION(data);
	GError *err = NULL;

	mm_bearer_disconnect_finish(MM_BEARER(obj), res, &err);
	if (err != NULL) {
		g_printerr("Failed to disconnect: %s\n", err->message);
		g_signal_emit(conn, conn_signals[DISCONNECT_ERR], 0, err);
		g_clear_error(&err);
	}

	g_object_unref(conn);
}

void qvd_connection_disconnect(QVDConnection *conn)
{
	g_assert(conn != NULL);
	g_assert(conn->bearer != NULL);

	conn->requested = FALSE;

	if (conn->bearer != NULL)
		mm_bearer_disconnect(conn->bearer, NULL, on_disconnect, g_object_ref(conn));
}

static void on_bearer_properties_changed(GDBusProxy* bearer,
					 GVariant* changed,
					 char** invalidated,
					 gpointer data)
{
	QVDConnection *conn = QVD_CONNECTION(data);

	g_assert(conn->ip_type == MM_BEARER_IP_FAMILY_IPV4
	      || conn->ip_type == MM_BEARER_IP_FAMILY_IPV6);
	if (conn->ip_type == MM_BEARER_IP_FAMILY_IPV4)
		try_notify_address(conn, changed, MM_BEARER_IP_FAMILY_IPV4, "Ip4Config");
	else
		try_notify_address(conn, changed, MM_BEARER_IP_FAMILY_IPV6, "Ip6Config");
}

void qvd_connection_set_bearer(QVDConnection *conn, MMBearer *bearer)
{
	g_assert(conn != NULL);
	g_assert(conn->bearer == NULL);

	conn->bearer = g_object_ref(bearer);

	g_signal_connect(bearer, "g-properties-changed",
			 G_CALLBACK(on_bearer_properties_changed), conn);

	if (conn->requested)
		mm_bearer_connect(conn->bearer, NULL, on_connect, conn);
}
