// SPDX-License-Identifier: GPL-2.0-or-later
/*
 *
 *  BlueZ - Bluetooth protocol stack for Linux
 *
 *  Copyright 2024 NXP
 *
 *
 */

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

#define _GNU_SOURCE
#include <stdio.h>
#include <stdbool.h>
#include <inttypes.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>

#include <glib.h>

#include "gdbus/gdbus.h"

#include "bluetooth/bluetooth.h"
#include "bluetooth/uuid.h"

#include "src/shared/util.h"
#include "src/shared/shell.h"
#include "src/shared/io.h"
#include "src/shared/queue.h"
#include "print.h"
#include "assistant.h"

/* String display constants */
#define COLORED_NEW	COLOR_GREEN "NEW" COLOR_OFF
#define COLORED_CHG	COLOR_YELLOW "CHG" COLOR_OFF
#define COLORED_DEL	COLOR_RED "DEL" COLOR_OFF

#define MEDIA_ASSISTANT_INTERFACE "org.bluez.MediaAssistant1"

#define BCODE_LEN		16

struct assistant_config {
	GDBusProxy *proxy;	/* DBus object reference */
	char *state;		/* Assistant state */
	char *device;		/* Device address */
	struct iovec *meta;	/* Stream metadata LTVs */
	struct bt_iso_qos qos;	/* Stream QoS parameters */
};

static DBusConnection *dbus_conn;

static GList *assistants;

static void assistant_menu_pre_run(const struct bt_shell_menu *menu);

static char *proxy_description(GDBusProxy *proxy, const char *title,
						const char *description)
{
	const char *path;

	path = g_dbus_proxy_get_path(proxy);

	return g_strdup_printf("%s%s%s%s %s ",
					description ? "[" : "",
					description ? : "",
					description ? "] " : "",
					title, path);
}

static void print_assistant(GDBusProxy *proxy, const char *description)
{
	char *str;

	str = proxy_description(proxy, "Assistant", description);

	bt_shell_printf("%s\n", str);

	g_free(str);
}

static void assistant_added(GDBusProxy *proxy)
{
	assistants = g_list_append(assistants, proxy);

	print_assistant(proxy, COLORED_NEW);
}

static void proxy_added(GDBusProxy *proxy, void *user_data)
{
	const char *interface;

	interface = g_dbus_proxy_get_interface(proxy);

	if (!strcmp(interface, MEDIA_ASSISTANT_INTERFACE))
		assistant_added(proxy);
}

static void assistant_removed(GDBusProxy *proxy)
{
	assistants = g_list_remove(assistants, proxy);

	print_assistant(proxy, COLORED_DEL);
}

static void proxy_removed(GDBusProxy *proxy, void *user_data)
{
	const char *interface;

	interface = g_dbus_proxy_get_interface(proxy);

	if (!strcmp(interface, MEDIA_ASSISTANT_INTERFACE))
		assistant_removed(proxy);
}

static void assistant_property_changed(GDBusProxy *proxy, const char *name,
						DBusMessageIter *iter)
{
	char *str;

	str = proxy_description(proxy, "Assistant", COLORED_CHG);
	print_iter(str, name, iter);
	g_free(str);
}

static void property_changed(GDBusProxy *proxy, const char *name,
					DBusMessageIter *iter, void *user_data)
{
	const char *interface;

	interface = g_dbus_proxy_get_interface(proxy);

	if (!strcmp(interface, MEDIA_ASSISTANT_INTERFACE))
		assistant_property_changed(proxy, name, iter);
}

static void assistant_unregister(void *data)
{
	GDBusProxy *proxy = data;

	bt_shell_printf("Assistant %s unregistered\n",
				g_dbus_proxy_get_path(proxy));
}

static void disconnect_handler(DBusConnection *connection, void *user_data)
{
	g_list_free_full(assistants, assistant_unregister);
	assistants = NULL;
}

static uint8_t *str2bytearray(char *arg, size_t *val_len)
{
	uint8_t value[UINT8_MAX];
	char *entry;
	unsigned int i;

	for (i = 0; (entry = strsep(&arg, " \t")) != NULL; i++) {
		long val;
		char *endptr = NULL;

		if (*entry == '\0')
			continue;

		if (i >= G_N_ELEMENTS(value)) {
			bt_shell_printf("Too much data\n");
			return NULL;
		}

		val = strtol(entry, &endptr, 0);
		if (!endptr || *endptr != '\0' || val > UINT8_MAX) {
			bt_shell_printf("Invalid value at index %d\n", i);
			return NULL;
		}

		value[i] = val;
	}

	*val_len = i;

	return util_memdup(value, i);
}

static void append_qos(DBusMessageIter *iter, struct assistant_config *cfg)
{
	DBusMessageIter entry, var, dict;
	const char *key = "QoS";
	const char *bcode_key = "BCode";
	uint8_t *bcode = cfg->qos.bcast.bcode;

	dbus_message_iter_open_container(iter, DBUS_TYPE_DICT_ENTRY,
						NULL, &entry);

	dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key);

	dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
						"a{sv}", &var);

	dbus_message_iter_open_container(&var, DBUS_TYPE_ARRAY, "{sv}",
					&dict);

	g_dbus_dict_append_basic_array(&dict, DBUS_TYPE_STRING,
					&bcode_key, DBUS_TYPE_BYTE,
					&bcode, BCODE_LEN);

	dbus_message_iter_close_container(&var, &dict);
	dbus_message_iter_close_container(&entry, &var);
	dbus_message_iter_close_container(iter, &entry);
}

static void push_setup(DBusMessageIter *iter, void *user_data)
{
	struct assistant_config *cfg = user_data;
	DBusMessageIter dict;
	const char *meta = "Metadata";

	dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, "{sv}", &dict);

	if (cfg->meta)
		g_dbus_dict_append_basic_array(&dict, DBUS_TYPE_STRING, &meta,
				DBUS_TYPE_BYTE, &cfg->meta->iov_base,
				cfg->meta->iov_len);

	if (cfg->device)
		g_dbus_dict_append_entry(&dict, "Device", DBUS_TYPE_OBJECT_PATH,
						&cfg->device);

	if (cfg->qos.bcast.encryption)
		append_qos(&dict, cfg);

	dbus_message_iter_close_container(iter, &dict);
}

static void push_reply(DBusMessage *message, void *user_data)
{
	struct assistant_config *cfg = user_data;
	DBusError error;

	dbus_error_init(&error);

	if (dbus_set_error_from_message(&error, message)) {
		bt_shell_printf("Failed to push assistant: %s\n",
				error.name);

		dbus_error_free(&error);

		return bt_shell_noninteractive_quit(EXIT_FAILURE);
	}

	bt_shell_printf("Assistant %s pushed\n",
				g_dbus_proxy_get_path(cfg->proxy));

	free(cfg->meta);
	g_free(cfg);

	return bt_shell_noninteractive_quit(EXIT_SUCCESS);
}

static void assistant_set_bcode_cfg(const char *input, void *user_data)
{
	struct assistant_config *cfg = user_data;

	if (!strcasecmp(input, "a") || !strcasecmp(input, "auto")) {
		memset(cfg->qos.bcast.bcode, 0, BCODE_LEN);
	} else {
		if (strnlen(input, BCODE_LEN + 1) > BCODE_LEN) {
			bt_shell_printf("Input string too long %s\n", input);
			goto fail;
		}

		memcpy(cfg->qos.bcast.bcode, input, strlen(input));
	}

	if (!g_dbus_proxy_method_call(cfg->proxy, "Push",
					push_setup, push_reply,
					cfg, NULL)) {
		bt_shell_printf("Failed to push assistant\n");
		goto fail;
	}

	return;

fail:
	free(cfg->meta);
	g_free(cfg);

	return bt_shell_noninteractive_quit(EXIT_FAILURE);
}

static bool assistant_get_qos(struct assistant_config *cfg)
{
	DBusMessageIter iter, dict;
	const char *key;

	/* Get QoS property to check if the stream is encrypted */
	if (!g_dbus_proxy_get_property(cfg->proxy, "QoS", &iter))
		return false;

	if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY)
		return false;

	dbus_message_iter_recurse(&iter, &dict);

	while (dbus_message_iter_get_arg_type(&dict) != DBUS_TYPE_DICT_ENTRY) {
		DBusMessageIter entry, value;
		int var;

		dbus_message_iter_recurse(&dict, &entry);
		dbus_message_iter_get_basic(&entry, &key);

		dbus_message_iter_next(&entry);
		dbus_message_iter_recurse(&entry, &value);

		var = dbus_message_iter_get_arg_type(&value);

		if (!strcasecmp(key, "Encryption")) {
			if (var != DBUS_TYPE_BYTE)
				return false;

			dbus_message_iter_get_basic(&value,
						&cfg->qos.bcast.encryption);
		} else if (!strcasecmp(key, "BCode")) {
			DBusMessageIter array;
			struct iovec iov = {0};

			if (var != DBUS_TYPE_ARRAY)
				return false;

			dbus_message_iter_recurse(&value, &array);
			dbus_message_iter_get_fixed_array(&array,
							&iov.iov_base,
							(int *)&iov.iov_len);

			if (iov.iov_len != 16) {
				bt_shell_printf("Invalid size for BCode: "
						"%zu != 16\n", iov.iov_len);
				return false;
			}

			memcpy(cfg->qos.bcast.bcode, iov.iov_base, iov.iov_len);
		}
	}

	return true;
}

static void assistant_set_metadata_cfg(const char *input, void *user_data)
{
	struct assistant_config *cfg = user_data;
	uint8_t no_bcode[16] = {};

	if (!strcasecmp(input, "a") || !strcasecmp(input, "auto"))
		goto done;

	if (!cfg->meta)
		cfg->meta = g_new0(struct iovec, 1);

	cfg->meta->iov_base = str2bytearray((char *) input,
				&cfg->meta->iov_len);
	if (!cfg->meta->iov_base) {
		free(cfg->meta);
		cfg->meta = NULL;
	}

done:
	if (!assistant_get_qos(cfg))
		goto fail;

	if (cfg->qos.bcast.encryption &&
			!memcmp(cfg->qos.bcast.bcode, no_bcode, 16))
		/* Prompt user to enter the Broadcast Code to decrypt
		 * the stream
		 */
		bt_shell_prompt_input("Assistant",
				"Enter Broadcast Code (auto/value):",
				assistant_set_bcode_cfg, cfg);
	else
		if (!g_dbus_proxy_method_call(cfg->proxy, "Push",
						push_setup, push_reply,
						cfg, NULL)) {
			bt_shell_printf("Failed to push assistant\n");
			goto fail;
		}

	return;

fail:
	free(cfg->meta);
	g_free(cfg);

	return bt_shell_noninteractive_quit(EXIT_FAILURE);
}

static void assistant_set_device_cfg(const char *input, void *user_data)
{
	struct assistant_config *cfg = user_data;
	uint8_t no_bcode[16] = {};

	cfg->device = strdup(input);

	if (!assistant_get_qos(cfg))
		goto fail;

	if (cfg->qos.bcast.encryption &&
			!memcmp(cfg->qos.bcast.bcode, no_bcode, 16)) {
		/* Prompt user to enter the Broadcast Code to decrypt
		 * the stream
		 */
		bt_shell_prompt_input("Assistant",
				"Enter Broadcast Code (auto/value):",
				assistant_set_bcode_cfg, cfg);
	} else {
		if (!g_dbus_proxy_method_call(cfg->proxy, "Push",
						push_setup, push_reply,
						cfg, NULL)) {
			bt_shell_printf("Failed to push assistant\n");
			goto fail;
		}
	}

	return;

fail:
	free(cfg->device);
	free(cfg->meta);
	g_free(cfg);

	return bt_shell_noninteractive_quit(EXIT_FAILURE);
}

static void cmd_push_assistant(int argc, char *argv[])
{
	struct assistant_config *cfg;
	DBusMessageIter iter;

	cfg = new0(struct assistant_config, 1);
	if (!cfg)
		goto fail;

	/* Search for DBus object */
	cfg->proxy = g_dbus_proxy_lookup(assistants, NULL, argv[1],
						MEDIA_ASSISTANT_INTERFACE);
	if (!cfg->proxy) {
		bt_shell_printf("Assistant %s not found\n", argv[1]);
		goto fail;
	}

	if (g_dbus_proxy_get_property(cfg->proxy, "State", &iter)) {
		dbus_message_iter_get_basic(&iter, &cfg->state);

		if (!strcmp(cfg->state, "local")) {
			/* Prompt user to enter metadata */
			bt_shell_prompt_input("Assistant",
					"Enter Device (path):",
					assistant_set_device_cfg, cfg);
			return;
		}
	}
	/* Prompt user to enter metadata */
	bt_shell_prompt_input("Assistant",
			"Enter Metadata (auto/value):",
			assistant_set_metadata_cfg, cfg);

	return;

fail:
	g_free(cfg);
	return bt_shell_noninteractive_quit(EXIT_FAILURE);
}

static void cmd_list_assistant(int argc, char *argv[])
{
	GList *l;

	for (l = assistants; l; l = g_list_next(l)) {
		GDBusProxy *proxy = l->data;
		print_assistant(proxy, NULL);
	}

	return bt_shell_noninteractive_quit(EXIT_SUCCESS);
}

static void print_assistant_properties(GDBusProxy *proxy)
{
	bt_shell_printf("Transport %s\n", g_dbus_proxy_get_path(proxy));

	print_property(proxy, "State");
	print_property(proxy, "Metadata");
	print_property(proxy, "QoS");
}

static void print_assistants(void *data, void *user_data)
{
	print_assistant_properties(data);
}

static char *generic_generator(const char *text, int state, GList *source)
{
	static int index = 0;

	if (!source)
		return NULL;

	if (!state)
		index = 0;

	return g_dbus_proxy_path_lookup(source, &index, text);
}

static char *assistant_generator(const char *text, int state)
{
	return generic_generator(text, state, assistants);
}

static void cmd_show_assistant(int argc, char *argv[])
{
	GDBusProxy *proxy;

	/* Show all transports if no argument is given */
	if (argc != 2) {
		g_list_foreach(assistants, print_assistants, NULL);
		return bt_shell_noninteractive_quit(EXIT_SUCCESS);
	}

	proxy = g_dbus_proxy_lookup(assistants, NULL, argv[1],
					MEDIA_ASSISTANT_INTERFACE);
	if (!proxy) {
		bt_shell_printf("Assistant %s not found\n", argv[1]);
		return bt_shell_noninteractive_quit(EXIT_FAILURE);
	}

	print_assistant_properties(proxy);

	return bt_shell_noninteractive_quit(EXIT_SUCCESS);
}

static const struct bt_shell_menu assistant_menu = {
	.name = "assistant",
	.desc = "Media Assistant Submenu",
	.pre_run = assistant_menu_pre_run,
	.entries = {
	{ "list", NULL, cmd_list_assistant, "List available assistants" },
	{ "show", "[assistant]", cmd_show_assistant,
					"Assistant information",
					assistant_generator },
	{ "push", "<assistant>", cmd_push_assistant,
					"Send stream information to peer",
					assistant_generator },
	{} },
};

static GDBusClient * client;

void assistant_add_submenu(void)
{
	bt_shell_add_submenu(&assistant_menu);
}

static void assistant_menu_pre_run(const struct bt_shell_menu *menu)
{
	dbus_conn = bt_shell_get_env("DBUS_CONNECTION");
	if (!dbus_conn || client)
		return;

	client = g_dbus_client_new(dbus_conn, "org.bluez", "/org/bluez");

	g_dbus_client_set_proxy_handlers(client, proxy_added, proxy_removed,
							property_changed, NULL);
	g_dbus_client_set_disconnect_watch(client, disconnect_handler, NULL);
}

void assistant_remove_submenu(void)
{
	g_dbus_client_unref(client);
	client = NULL;
}

