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

#include <gio/gio.h>
#include <glib.h>
#include <libmm-glib.h>
#include <ModemManager-enums.h>
#include <stdio.h>

#include "qvd-connection.h"
#include "qvd-modem.h"

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

struct _QVDModem {
	GObject parent_instance;

	GDBusConnection *dbus;
	MMManager *manager;
	MMModem *modem;
	GHashTable *bearers;
	GPtrArray *pending_bearers;
};

struct qvd_pending_bearer {
	MMBearerProperties *props;
	GTask *task;
};

struct qvd_bearer_config {
	MMBearerIpFamily ip_type;
	gchar apn[256];
};

static void qvd_modem_initable_iface_init(GInitableIface *iface);

G_DEFINE_TYPE_WITH_CODE(QVDModem, qvd_modem, G_TYPE_OBJECT,
			G_IMPLEMENT_INTERFACE(G_TYPE_INITABLE,
					      qvd_modem_initable_iface_init))

static gboolean qvd_modem_initable_init(GInitable *initable,
					GCancellable *cancellable,
					GError **out_err);
static void qvd_modem_dispose(GObject *obj);

static guint modem_signals[N_SIGNALS];

static void qvd_modem_class_init(QVDModemClass *c)
{
	GObjectClass *obj_class = G_OBJECT_CLASS(c);

	obj_class->dispose = qvd_modem_dispose;

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

static void qvd_modem_initable_iface_init(GInitableIface *iface)
{
	iface->init = qvd_modem_initable_init;
}

static void qvd_modem_init(QVDModem *self)
{
}

static void create_bearer_ready(GObject *obj, GAsyncResult *res, gpointer data)
{
	GTask *task = G_TASK(data);
	QVDConnection *conn;
	GError *err = NULL;
	MMBearer *bearer;

	bearer = mm_modem_create_bearer_finish(MM_MODEM(obj), res, &err);
	if (err != NULL) {
		g_printerr("Failed to create bearer: %s\n", err->message);
		g_task_return_error(task, err);
		goto err;
	}

	conn = QVD_CONNECTION(g_task_get_task_data(task));

	qvd_connection_set_bearer(conn, bearer);

	g_task_return_boolean(task, TRUE);

err:
	g_object_unref(task);
}

static gboolean is_bearer_listed(QVDConnection *conn, GVariant *bearers)
{
	GVariantIter *iter;
	gchar *path;
	const gchar *conn_path;
	gboolean matched = FALSE;

	conn_path = qvd_connection_get_path(conn);
	if (conn_path == NULL)
		return TRUE;

	g_variant_get(bearers, "ao", &iter);

	while (g_variant_iter_loop(iter, "o", &path)) {
		if (!strcmp(path, conn_path)) {
			matched = TRUE;
			g_free(path);
			break;
		}
	}

	g_variant_iter_free(iter);

	return matched;
}

static void on_modem_props_changed(GDBusProxy *modem,
				   GVariant *changed,
				   char **invalidated,
				   gpointer data)
{
	struct qvd_bearer_config *config;
	QVDModem *ctx = QVD_MODEM(data);
	QVDConnection *conn;
	GHashTableIter tracked_bearers;
	GVariantIter *props;
	gpointer k, v;
	GVariant *prop;
	gchar *name;

	g_variant_get(changed, "a{sv}", &props);
	while (g_variant_iter_loop(props, "{sv}", &name, &prop)) {
		if (!strcmp(name, "Bearers"))
			break;
	}

	g_variant_iter_free(props);

	if (name == NULL || prop == NULL)
		return;

	g_free(name);

	g_hash_table_iter_init(&tracked_bearers, ctx->bearers);
	while (g_hash_table_iter_next(&tracked_bearers, &k, &v)) {
		config = k;
		conn = QVD_CONNECTION(v);

		if (!is_bearer_listed(conn, prop)) {
			g_signal_emit(ctx, modem_signals[ADDR_CHANGED], 0,
				      QVD_CONNECTION(v), config->ip_type, NULL);
			g_hash_table_iter_remove(&tracked_bearers);
			g_warning("Bearer removed remotely");
		}
	}

	g_variant_unref(prop);
}

static void on_object_added(QVDConnection *conn,
			    GDBusObject *object,
			    gpointer data)
{
	MMModem *modem;
	const MMModemPortInfo *ports;
	struct qvd_pending_bearer *pending;
	guint n_ports = 0, i;
	gboolean is_qrtr = FALSE;
	QVDModem *ctx = QVD_MODEM(data);

	modem = mm_object_get_modem(MM_OBJECT(object));
	if (modem == NULL)
		return;

	mm_modem_peek_ports(modem, &ports, &n_ports);

	for (i = 0; i < n_ports; i++) {
		if (ports[i].type == MM_MODEM_PORT_TYPE_QMI
		 && !strcmp(ports[i].name, "qrtr0")) {
			is_qrtr = TRUE;
			break;
		}
	}

	if (is_qrtr) {
		g_info("ModemManager QRTR modem detected\n");
		ctx->modem = g_object_ref(modem);
		g_signal_connect(modem, "g-properties-changed",
				 G_CALLBACK(on_modem_props_changed), ctx);

		for (i = 0; i < ctx->pending_bearers->len; i++) {
			pending = ctx->pending_bearers->pdata[i];
			mm_modem_create_bearer(ctx->modem, pending->props, NULL, create_bearer_ready, pending->task);
			g_object_unref(pending->props);
		}

		g_ptr_array_unref(g_steal_pointer(&ctx->pending_bearers));
		ctx->pending_bearers = g_ptr_array_new_full(0, g_free);
	}
}

static void send_bearer_disconnected(gpointer k, gpointer v, gpointer data)
{
	struct qvd_bearer_config *config = k;
	QVDModem *ctx = QVD_MODEM(data);
	QVDConnection *conn = QVD_CONNECTION(v);

	g_signal_emit(ctx, modem_signals[ADDR_CHANGED], 0, conn, config->ip_type, NULL);
}


static void on_object_removed(QVDConnection *conn,
			      GDBusObject *object,
			      gpointer data)
{
	QVDModem *ctx = QVD_MODEM(data);
	const gchar *removed_path, *path;

	removed_path = mm_object_get_path(MM_OBJECT(object));
	path = mm_modem_get_path(ctx->modem);

	if (strcmp(path, removed_path))
		return;

	g_warning("ModemManager QRTR modem removed");

	g_hash_table_foreach(ctx->bearers, send_bearer_disconnected, ctx);
	g_hash_table_remove_all(ctx->bearers);
	g_clear_object(&ctx->modem);
}

static void find_modem(gpointer memb, gpointer data)
{
	MMModem **out = data;
	MMModem *modem;
	const MMModemPortInfo *ports;
	guint n_ports = 0, i;
	gboolean is_qrtr = FALSE;

	modem = mm_object_get_modem(memb);
	if (modem == NULL)
		return;

	mm_modem_peek_ports(modem, &ports, &n_ports);

	for (i = 0; i < n_ports; i++) {
		if (ports[i].type == MM_MODEM_PORT_TYPE_QMI
		 && !strcmp(ports[i].name, "qrtr0")) {
			is_qrtr = TRUE;
			break;
		}
	}

	if (is_qrtr)
		*out = g_object_ref(modem);
}

static guint qvd_bearer_config_hash(gconstpointer key)
{
	const struct qvd_bearer_config *conf = key;

	return g_int_hash(&conf->ip_type) ^ g_str_hash(conf->apn);
}

static gboolean qvd_bearer_config_equal(gconstpointer va, gconstpointer vb)
{
	const struct qvd_bearer_config *a = va;
	const struct qvd_bearer_config *b = vb;

	return g_int_equal(&a->ip_type, &b->ip_type) && g_str_equal(a->apn, b->apn);
}

static gboolean qvd_modem_initable_init(GInitable *initable,
					GCancellable *cancellable,
					GError **out_err)
{
	QVDModem *ctx = QVD_MODEM(initable);
	GError *err = NULL;
	GList *objs;

	ctx->dbus = g_bus_get_sync(G_BUS_TYPE_SYSTEM, cancellable, &err);
	if (err != NULL) {
		g_propagate_error(out_err, err);
		return FALSE;
	}

	ctx->manager = mm_manager_new_sync(ctx->dbus,
					   G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START,
					   NULL, &err);
	if (err != NULL) {
		g_propagate_error(out_err, err);
		return FALSE;
	}

	objs = g_dbus_object_manager_get_objects(G_DBUS_OBJECT_MANAGER(ctx->manager));
	g_list_foreach(objs, find_modem, &ctx->modem);
	g_list_free_full(objs, g_object_unref);
	if (ctx->modem != NULL)
		g_signal_connect(ctx->modem, "g-properties-changed",
				 G_CALLBACK(on_modem_props_changed), ctx);
	else
		g_warning("ModemManager QRTR modem not detected on startup\n");

	g_signal_connect(ctx->manager, "object-added", G_CALLBACK(on_object_added), ctx);
	g_signal_connect(ctx->manager, "object-removed", G_CALLBACK(on_object_removed), ctx);

	ctx->bearers = g_hash_table_new_full(qvd_bearer_config_hash, qvd_bearer_config_equal,
					     g_free, g_object_unref);
	ctx->pending_bearers = g_ptr_array_new_full(0, g_free);

	return TRUE;
}

static void qvd_modem_dispose(GObject *obj)
{
	QVDModem *ctx = QVD_MODEM(obj);

	if (ctx->bearers != NULL)
		g_hash_table_destroy(g_steal_pointer(&ctx->bearers));

	if (ctx->pending_bearers != NULL)
		g_ptr_array_unref(g_steal_pointer(&ctx->pending_bearers));

	g_clear_object(&ctx->manager);
	g_clear_object(&ctx->dbus);
}

QVDModem *qvd_modem_new(GError **err)
{
	return g_initable_new(QVD_TYPE_MODEM, NULL, err, NULL);
}

static void on_address_changed(QVDConnection *conn,
			       MMBearerIpFamily ip_type,
			       const gchar *addr,
			       gpointer data)
{
	QVDModem *ctx = QVD_MODEM(data);

	g_signal_emit(ctx, modem_signals[ADDR_CHANGED], 0, conn, ip_type, addr);
}

static void on_connect_error(QVDConnection *conn, const GError *err, gpointer data)
{
	QVDModem *ctx = QVD_MODEM(data);

	g_signal_emit(ctx, modem_signals[CONNECT_ERR], 0, conn, err);
}

static void on_disconnect_error(QVDConnection *conn, const GError *err, gpointer data)
{
	QVDModem *ctx = QVD_MODEM(data);

	g_signal_emit(ctx, modem_signals[DISCONNECT_ERR], 0, conn, err);
}

gboolean qvd_modem_get_connection_finish(QVDModem *ctx, GAsyncResult *res, GError **err)
{
	return g_task_propagate_boolean(G_TASK(res), err);
}

QVDConnection *qvd_modem_get_connection(QVDModem *ctx,
					const gchar *apn,
					MMBearerIpFamily ip_family,
					GCancellable *cancellable,
					GAsyncReadyCallback cb,
					gpointer data)
{
	struct qvd_bearer_config *conf;
	struct qvd_pending_bearer *pending;
	GTask *task;
	QVDConnection *conn;
	MMBearerProperties *props;

	g_assert(ctx != NULL);

	task = g_task_new(ctx, cancellable, cb, data);

	conf = g_malloc(sizeof(struct qvd_bearer_config));
	conf->ip_type = ip_family;
	strncpy(conf->apn, apn, 255);
	conf->apn[255] = '\0';

	if (!g_hash_table_contains(ctx->bearers, conf)) {
		conn = qvd_connection_new(ip_family);
		g_signal_connect(conn, "qvd-address-changed",
				 G_CALLBACK(on_address_changed), ctx);
		g_signal_connect(conn, "qvd-connect-error",
				 G_CALLBACK(on_connect_error), ctx);
		g_signal_connect(conn, "qvd-disconnect-error",
				 G_CALLBACK(on_disconnect_error), ctx);
		g_hash_table_insert(ctx->bearers, conf, conn);

		props = mm_bearer_properties_new();
		mm_bearer_properties_set_apn(props, apn);
		mm_bearer_properties_set_ip_type(props, ip_family);
		mm_bearer_properties_set_allow_roaming(props, FALSE);

		g_task_set_task_data(task, conn, NULL);

		if (ctx->modem == NULL) {
			g_warning("Deferring connection until ModemManager QRTR modem can be detected\n");

			pending = g_malloc(sizeof(*pending));
			pending->props = props;
			pending->task = task;

			g_ptr_array_add(ctx->pending_bearers, pending);
		} else {
			mm_modem_create_bearer(ctx->modem, props, NULL, create_bearer_ready, task);
			g_object_unref(props);
		}
	} else {
		conn = g_hash_table_lookup(ctx->bearers, conf);
		g_task_return_boolean(task, true);
		g_free(conf);
	}

	return g_object_ref(conn);
}
