// SPDX-License-Identifier: GPL-2.0-or-later
/*
 *
 *  BlueZ - Bluetooth protocol stack for Linux
 *
 *  Copyright (C) 2004-2010  Marcel Holtmann <marcel@holtmann.org>
 *
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <errno.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/utsname.h>

#include "bluetooth/bluetooth.h"
#include "bluetooth/sdp.h"

#include "gdbus/gdbus.h"

#include "src/dbus-common.h"
#include "src/plugin.h"
#include "src/adapter.h"
#include "src/log.h"

/* http://www.bluetooth.org/Technical/AssignedNumbers/baseband.htm */

#define MAJOR_CLASS_MISCELLANEOUS	0x00
#define MAJOR_CLASS_COMPUTER		0x01

#define MINOR_CLASS_UNCATEGORIZED	0x00
#define MINOR_CLASS_DESKTOP		0x01
#define MINOR_CLASS_SERVER		0x02
#define MINOR_CLASS_LAPTOP		0x03
#define MINOR_CLASS_HANDHELD		0x04
#define MINOR_CLASS_PALM_SIZED		0x05
#define MINOR_CLASS_WEARABLE		0x06
#define MINOR_CLASS_TABLET		0x07

static uint8_t major_class = MAJOR_CLASS_MISCELLANEOUS;
static uint8_t minor_class = MINOR_CLASS_UNCATEGORIZED;

static char *pretty_hostname    = NULL;
static char *static_hostname    = NULL;
static char *transient_hostname = NULL;
static guint hostname_id = 0;

/*
 * Fallback to static hostname only if empty pretty hostname was already
 * received.
 */
static const char *get_hostname(void)
{
	if (pretty_hostname) {
		if (g_str_equal(pretty_hostname, "") == FALSE)
			return pretty_hostname;

		if (static_hostname &&
				g_str_equal(static_hostname, "") == FALSE)
			return static_hostname;

		if (transient_hostname &&
				g_str_equal(transient_hostname, "") == FALSE)
			return transient_hostname;
	}

	return NULL;
}

static void update_name(struct btd_adapter *adapter, gpointer user_data)
{
	const char *hostname = get_hostname();

	if (hostname == NULL)
		return;

	if (btd_adapter_is_default(adapter)) {
		DBG("name: %s", hostname);

		adapter_set_name(adapter, hostname);
	} else {
		uint16_t index = btd_adapter_get_index(adapter);
		char *str;

		/* Avoid "some device #0" names, start at #1 */
		str = g_strdup_printf("%s #%u", hostname, index + 1);

		DBG("name: %s", str);

		adapter_set_name(adapter, str);

		g_free(str);
	}
}

static void update_class(struct btd_adapter *adapter, gpointer user_data)
{
	if (major_class == MAJOR_CLASS_MISCELLANEOUS)
		return;

	DBG("major: 0x%02x minor: 0x%02x", major_class, minor_class);

	btd_adapter_set_class(adapter, major_class, minor_class);
}

static const struct {
	const char *chassis;
	uint8_t major_class;
	uint8_t minor_class;
} chassis_table[] = {
	{ "desktop",  MAJOR_CLASS_COMPUTER, MINOR_CLASS_DESKTOP  },
	{ "server",   MAJOR_CLASS_COMPUTER, MINOR_CLASS_SERVER   },
	{ "laptop",   MAJOR_CLASS_COMPUTER, MINOR_CLASS_LAPTOP   },
	{ "handset",  MAJOR_CLASS_COMPUTER, MINOR_CLASS_HANDHELD },
	{ "tablet",   MAJOR_CLASS_COMPUTER, MINOR_CLASS_TABLET   },
	{ }
};

static void property_changed(GDBusProxy *proxy, const char *name,
					DBusMessageIter *iter, void *user_data)
{
	if (g_str_equal(name, "PrettyHostname") == TRUE) {
		if (iter == NULL) {
			g_dbus_proxy_refresh_property(proxy, name);
			return;
		}

		if (dbus_message_iter_get_arg_type(iter) == DBUS_TYPE_STRING) {
			const char *str;

			dbus_message_iter_get_basic(iter, &str);

			DBG("pretty hostname: '%s'", str);

			g_free(pretty_hostname);
			pretty_hostname = g_strdup(str);

			adapter_foreach(update_name, NULL);
		}
	} else if (g_str_equal(name, "StaticHostname") == TRUE) {
		if (iter == NULL) {
			g_dbus_proxy_refresh_property(proxy, name);
			return;
		}

		if (dbus_message_iter_get_arg_type(iter) == DBUS_TYPE_STRING) {
			const char *str;

			dbus_message_iter_get_basic(iter, &str);

			DBG("static hostname: '%s'", str);

			g_free(static_hostname);
			static_hostname = g_strdup(str);

			adapter_foreach(update_name, NULL);
		}
	} else if (g_str_equal(name, "Chassis") == TRUE) {
		if (iter == NULL) {
			g_dbus_proxy_refresh_property(proxy, name);
			return;
		}

		if (dbus_message_iter_get_arg_type(iter) == DBUS_TYPE_STRING) {
			const char *str;
			int i;

			dbus_message_iter_get_basic(iter, &str);

			DBG("chassis: '%s'", str);

			for (i = 0; chassis_table[i].chassis; i++) {
				if (strcmp(chassis_table[i].chassis, str))
					continue;

				major_class = chassis_table[i].major_class;
				minor_class = chassis_table[i].minor_class;

				adapter_foreach(update_class, NULL);
				break;
			}
		}
	}
}

static void read_transient_hostname(void)
{
	struct utsname u;

	if (uname(&u) != 0) {
		g_free(transient_hostname);
		transient_hostname = NULL;
		DBG("failed to read transient hostname");
		return;
	}

	g_free(transient_hostname);
	transient_hostname = g_strdup(u.nodename);

	DBG("read transient hostname: '%s'", transient_hostname);
}

static gboolean hostname_cb(GIOChannel *io, GIOCondition cond,
							gpointer user_data)
{
	DBG("transient hostname changed");
	read_transient_hostname();
	adapter_foreach(update_class, NULL);
	return TRUE;
}

static int hostname_probe(struct btd_adapter *adapter)
{
	DBG("");

	update_name(adapter, NULL);
	update_class(adapter, NULL);

	return 0;
}

static void hostname_remove(struct btd_adapter *adapter)
{
	DBG("");
}

static struct btd_adapter_driver hostname_driver = {
	.name	= "hostname",
	.probe	= hostname_probe,
	.remove	= hostname_remove,
};

static void read_dmi_fallback(void)
{
	char *contents;
	int i, type;
	const char *str;

	if (g_file_get_contents("/sys/class/dmi/id/chassis_type",
					&contents, NULL, NULL) == FALSE)
		return;

	type = atoi(contents);
	g_free(contents);
	if (type < 0 || type > 0x1D)
		return;

	/* from systemd hostname chassis list */
	switch (type) {
	case 0x3:
	case 0x4:
	case 0x6:
	case 0x7:
		str = "desktop";
		break;
	case 0x8:
	case 0x9:
	case 0xA:
	case 0xE:
		str = "laptop";
		break;
	case 0xB:
		str = "handset";
		break;
	case 0x11:
	case 0x1C:
		str = "server";
		break;
	default:
		return;
	}

	DBG("chassis: %s", str);

	for (i = 0; chassis_table[i].chassis; i++) {
		if (!strcmp(chassis_table[i].chassis, str)) {
			major_class = chassis_table[i].major_class;
			minor_class = chassis_table[i].minor_class;
			break;
		}
	}

	DBG("major: 0x%02x minor: 0x%02x", major_class, minor_class);
}

static GDBusClient *hostname_client = NULL;
static GDBusProxy *hostname_proxy = NULL;

static int hostname_init(void)
{
	DBusConnection *conn = btd_get_dbus_connection();
	int fd;
	int err;

	read_dmi_fallback();
	read_transient_hostname();

	hostname_client = g_dbus_client_new(conn, "org.freedesktop.hostname1",
						"/org/freedesktop/hostname1");
	if (!hostname_client)
		return -EIO;

	hostname_proxy = g_dbus_proxy_new(hostname_client,
						"/org/freedesktop/hostname1",
						"org.freedesktop.hostname1");
	if (!hostname_proxy) {
		g_dbus_client_unref(hostname_client);
		hostname_client = NULL;
		return -EIO;
	}

	g_dbus_proxy_set_property_watch(hostname_proxy, property_changed, NULL);

	err = btd_register_adapter_driver(&hostname_driver);
	if (err < 0) {
		g_dbus_proxy_unref(hostname_proxy);
		hostname_proxy = NULL;
		g_dbus_client_unref(hostname_client);
		hostname_client = NULL;
	}

	fd = open("/proc/sys/kernel/hostname", O_RDONLY);
	if (fd < 0) {
		error("open(/proc/sys/kernel/hostname): %s (%d)",
					strerror(errno), errno);
	} else {
		GIOChannel *io = g_io_channel_unix_new(fd);

		hostname_id = g_io_add_watch(io, G_IO_ERR, hostname_cb, NULL);
		g_io_channel_unref(io);
	}

	return err;
}

static void hostname_exit(void)
{
	btd_unregister_adapter_driver(&hostname_driver);

	if (hostname_proxy) {
		g_dbus_proxy_unref(hostname_proxy);
		hostname_proxy = NULL;
	}

	if (hostname_client) {
		g_dbus_client_unref(hostname_client);
		hostname_client = NULL;
	}

	if (hostname_id != 0) {
		g_source_remove(hostname_id);
		hostname_id = 0;
	}

	g_free(pretty_hostname);
	g_free(static_hostname);
	g_free(transient_hostname);
}

BLUETOOTH_PLUGIN_DEFINE(hostname, VERSION, BLUETOOTH_PLUGIN_PRIORITY_DEFAULT,
						hostname_init, hostname_exit)
