// SPDX-License-Identifier: LGPL-2.1-or-later
/*
 *
 *  BlueZ - Bluetooth protocol stack for Linux
 *
 *  Copyright (C) 2025  Pauli Virtanen. All rights reserved.
 *
 */

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

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

#include "src/shared/queue.h"
#include "src/shared/util.h"
#include "src/shared/timeout.h"
#include "src/shared/att.h"
#include "src/shared/gatt-db.h"
#include "src/shared/gatt-server.h"
#include "src/shared/gatt-client.h"
#include "src/shared/tmap.h"
#include "src/shared/bap.h"

#define DBG(_tmap, fmt, arg...) \
	tmap_debug(_tmap, "%s:%s() " fmt, __FILE__, __func__, ## arg)

struct bt_tmas_db {
	struct gatt_db *db;
	struct gatt_db_attribute *service;
	struct gatt_db_attribute *role;
	uint16_t role_value;
};

struct bt_tmap {
	int ref_count;
	struct bt_gatt_client *client;
	struct bt_tmas_db db;

	int idle_id;
	bt_tmap_ready_func_t ready_func;
	void *ready_data;

	bt_tmap_debug_func_t debug_func;
	bt_tmap_destroy_func_t debug_destroy;
	void *debug_data;
};

static struct queue *instances;

static void tmap_free(void *data)
{
	struct bt_tmap *tmap = data;

	if (tmap->client) {
		bt_gatt_client_idle_unregister(tmap->client, tmap->idle_id);
		bt_gatt_client_unref(tmap->client);
	} else {
		gatt_db_remove_service(tmap->db.db, tmap->db.service);
		gatt_db_unref(tmap->db.db);
	}

	queue_remove(instances, tmap);
	if (queue_isempty(instances)) {
		queue_destroy(instances, NULL);
		instances = NULL;
	}

	free(tmap);
}

struct bt_tmap *bt_tmap_ref(struct bt_tmap *tmap)
{
	if (!tmap)
		return NULL;

	__sync_fetch_and_add(&tmap->ref_count, 1);

	return tmap;
}

void bt_tmap_unref(struct bt_tmap *tmap)
{
	if (!tmap)
		return;

	if (__sync_sub_and_fetch(&tmap->ref_count, 1))
		return;

	tmap_free(tmap);
}

static void tmap_debug(struct bt_tmap *tmap, const char *format, ...)
{
	va_list ap;

	if (!tmap || !format || !tmap->debug_func)
		return;

	va_start(ap, format);
	util_debug_va(tmap->debug_func, tmap->debug_data, format, ap);
	va_end(ap);
}

bool bt_tmap_set_debug(struct bt_tmap *tmap, bt_tmap_debug_func_t cb,
		void *user_data, bt_tmap_destroy_func_t destroy)
{
	if (!tmap)
		return false;

	if (tmap->debug_destroy)
		tmap->debug_destroy(tmap->debug_data);

	tmap->debug_func = cb;
	tmap->debug_destroy = destroy;
	tmap->debug_data = user_data;

	return true;
}

uint16_t bt_tmap_get_role(struct bt_tmap *tmap)
{
	if (!tmap)
		return 0;

	return tmap->db.role_value;
}

/*
 * TMA Client
 */

static void tmap_role_read(bool success, uint8_t att_ecode,
					const uint8_t *value, uint16_t length,
					void *user_data)
{
	struct bt_tmap *tmap = user_data;
	struct iovec iov = { .iov_base = (void *)value, .iov_len = length };
	uint16_t role;

	if (!success) {
		DBG(tmap, "Unable to read Role: error 0x%02x", att_ecode);
		return;
	}

	if (!util_iov_pull_le16(&iov, &role)) {
		DBG(tmap, "Invalid Role");
		return;
	}

	DBG(tmap, "Role 0x%x", role);
	tmap->db.role_value = role & BT_TMAP_ROLE_MASK;
}

static void foreach_tmap_char(struct gatt_db_attribute *attr, void *user_data)
{
	struct bt_tmap *tmap = user_data;
	uint16_t value_handle;
	bt_uuid_t uuid, uuid_role;

	if (!gatt_db_attribute_get_char_data(attr, NULL, &value_handle,
						NULL, NULL, &uuid))
		return;

	bt_uuid16_create(&uuid_role, TMAP_ROLE_CHRC_UUID);

	if (!bt_uuid_cmp(&uuid, &uuid_role)) {
		DBG(tmap, "TMAS Role Char found: handle 0x%04x", value_handle);
		bt_gatt_client_read_value(tmap->client, value_handle,
					tmap_role_read, tmap, NULL);
		return;
	}
}

static void foreach_tmap_service(struct gatt_db_attribute *attr,
						void *user_data)
{
	struct bt_tmap *tmap = user_data;

	gatt_db_service_set_claimed(attr, true);
	gatt_db_service_foreach_char(attr, foreach_tmap_char, tmap);
}

static void tmap_idle(void *data)
{
	struct bt_tmap *tmap = data;

	tmap->idle_id = 0;

	if (!instances)
		instances = queue_new();
	queue_push_tail(instances, tmap);

	if (tmap->ready_func)
		tmap->ready_func(tmap, tmap->ready_data);
}

struct bt_tmap *bt_tmap_attach(struct bt_gatt_client *client,
				bt_tmap_ready_func_t ready, void *user_data)
{
	struct bt_tmap *tmap;
	bt_uuid_t uuid;

	if (!client)
		return NULL;

	client = bt_gatt_client_clone(client);
	if (!client)
		return NULL;

	tmap = new0(struct bt_tmap, 1);
	tmap->client = client;
	tmap->ready_func = ready;
	tmap->ready_data = user_data;
	tmap->db.db = bt_gatt_client_get_db(tmap->client);

	bt_uuid16_create(&uuid, TMAS_UUID);
	gatt_db_foreach_service(tmap->db.db, &uuid, foreach_tmap_service, tmap);

	tmap->idle_id = bt_gatt_client_idle_register(tmap->client, tmap_idle,
								tmap, NULL);

	return bt_tmap_ref(tmap);
}

/*
 * TMAS Service
 */

static void tmas_role_read(struct gatt_db_attribute *attrib,
					unsigned int id, uint16_t offset,
					uint8_t opcode, struct bt_att *att,
					void *user_data)
{
	struct bt_tmas_db *db = user_data;
	uint8_t role[2];

	put_le16(db->role_value, role);

	gatt_db_attribute_read_result(attrib, id, 0, role, sizeof(role));
}

static bool match_db(const void *data, const void *match_data)
{
	const struct bt_tmap *tmap = data;

	return tmap->db.db == match_data;
}

struct bt_tmap *bt_tmap_find(struct gatt_db *db)
{
	return db ? queue_find(instances, match_db, db) : NULL;
}

struct bt_tmap *bt_tmap_add_db(struct gatt_db *db)
{
	struct bt_tmap *tmap;
	bt_uuid_t uuid;

	if (!db || queue_find(instances, match_db, db))
		return NULL;

	tmap = new0(struct bt_tmap, 1);
	tmap->db.db = gatt_db_ref(db);

	bt_uuid16_create(&uuid, TMAS_UUID);
	tmap->db.service = gatt_db_add_service(db, &uuid, true, 3);

	bt_uuid16_create(&uuid, TMAP_ROLE_CHRC_UUID);
	tmap->db.role = gatt_db_service_add_characteristic(
					tmap->db.service,
					&uuid,
					BT_ATT_PERM_READ,
					BT_GATT_CHRC_PROP_READ,
					tmas_role_read, NULL,
					&tmap->db);

	if (!instances)
		instances = queue_new();
	queue_push_tail(instances, tmap);

	return bt_tmap_ref(tmap);
}

void bt_tmap_set_role(struct bt_tmap *tmap, uint16_t role)
{
	if (!tmap || tmap->client)
		return;

	role &= BT_TMAP_ROLE_MASK;
	if (role == tmap->db.role_value)
		return;

	DBG(tmap, "set role 0x%02x", role);

	tmap->db.role_value = role;

	/* Expose when first set and present. Role does not have Notify. */
	gatt_db_service_set_active(tmap->db.service, role != 0);
}
