// 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;
};

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 on_object_added(QVDConnection *conn,
			    GDBusObject *object,
			    gpointer data)
{
	MMModem *modem;
	const MMModemPortInfo *ports;
	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);
	}
}

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_warning("ModemManager QRTR modem not detected on startup\n");

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

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

	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));

	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);
}

static void create_bearer_ready(GObject *obj, GAsyncResult *res, gpointer data)
{
	QVDConnection *conn = QVD_CONNECTION(data);
	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_clear_error(&err);
		return;
	}

	qvd_connection_set_bearer(conn, bearer);
}

QVDConnection *qvd_modem_get_connection(QVDModem *ctx,
					const gchar *apn,
					MMBearerIpFamily ip_family)
{
	struct qvd_bearer_config *conf;
	QVDConnection *conn;
	MMBearerProperties *props;

	g_assert(ctx != NULL);

	if (ctx->modem == NULL)
		return NULL;

	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);
		mm_modem_create_bearer(ctx->modem, props, NULL, create_bearer_ready, conn);
		g_object_unref(props);
	} else {
		conn = g_hash_table_lookup(ctx->bearers, conf);
		g_free(conf);
	}

	return g_object_ref(conn);
}
