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

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

#include <stdlib.h>
#include <stdint.h>
#include <errno.h>
#include <unistd.h>
#include <assert.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include <glib.h>
#include <dbus/dbus.h>

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

#include "gdbus/gdbus.h"

#include "src/adapter.h"
#include "src/device.h"
#include "src/profile.h"
#include "src/service.h"
#include "src/log.h"
#include "src/error.h"
#include "src/sdpd.h"
#include "src/uuid-helper.h"
#include "src/dbus-common.h"

#include "avctp.h"
#include "control.h"
#include "player.h"

static GSList *devices = NULL;

struct control {
	struct btd_device *dev;
	struct avctp *session;
	struct btd_service *target;
	struct btd_service *remote;
	unsigned int avctp_id;
	const char *player;
};

static void state_changed(struct btd_device *dev, avctp_state_t old_state,
					avctp_state_t new_state, int err,
					void *user_data)
{
	struct control *control = user_data;
	DBusConnection *conn = btd_get_dbus_connection();
	const char *path = device_get_path(dev);

	switch (new_state) {
	case AVCTP_STATE_DISCONNECTED:
		control->session = NULL;
		control->player = NULL;

		g_dbus_emit_property_changed(conn, path,
					AUDIO_CONTROL_INTERFACE, "Connected");
		g_dbus_emit_property_changed(conn, path,
					AUDIO_CONTROL_INTERFACE, "Player");

		break;
	case AVCTP_STATE_CONNECTING:
		if (control->session)
			break;

		control->session = avctp_get(dev);

		break;
	case AVCTP_STATE_CONNECTED:
		g_dbus_emit_property_changed(conn, path,
					AUDIO_CONTROL_INTERFACE, "Connected");
		break;
	case AVCTP_STATE_BROWSING_CONNECTING:
	case AVCTP_STATE_BROWSING_CONNECTED:
	default:
		return;
	}
}

int control_connect(struct btd_service *service)
{
	struct control *control = btd_service_get_user_data(service);

	if (control->session)
		return -EALREADY;

	control->session = avctp_connect(control->dev);
	if (!control->session)
		return -EIO;

	return 0;
}

int control_disconnect(struct btd_service *service)
{
	struct control *control = btd_service_get_user_data(service);

	if (!control || !control->session)
		return -ENOTCONN;

	avctp_disconnect(control->session);

	return 0;
}

static DBusMessage *key_pressed(DBusConnection *conn, DBusMessage *msg,
					uint8_t op, bool hold, void *data)
{
	struct control *control = data;
	int err;

	if (!control->session)
		return btd_error_not_connected(msg);

	if (!control->target)
		return btd_error_not_supported(msg);

	err = avctp_send_passthrough(control->session, op, hold);
	if (err < 0)
		return btd_error_failed(msg, strerror(-err));

	return dbus_message_new_method_return(msg);
}

static DBusMessage *control_volume_up(DBusConnection *conn, DBusMessage *msg,
								void *data)
{
	return key_pressed(conn, msg, AVC_VOLUME_UP, false, data);
}

static DBusMessage *control_volume_down(DBusConnection *conn, DBusMessage *msg,
								void *data)
{
	return key_pressed(conn, msg, AVC_VOLUME_DOWN, false, data);
}

static DBusMessage *control_play(DBusConnection *conn, DBusMessage *msg,
								void *data)
{
	return key_pressed(conn, msg, AVC_PLAY, false, data);
}

static DBusMessage *control_pause(DBusConnection *conn, DBusMessage *msg,
								void *data)
{
	return key_pressed(conn, msg, AVC_PAUSE, false, data);
}

static DBusMessage *control_stop(DBusConnection *conn, DBusMessage *msg,
								void *data)
{
	return key_pressed(conn, msg, AVC_STOP, false, data);
}

static DBusMessage *control_next(DBusConnection *conn, DBusMessage *msg,
								void *data)
{
	return key_pressed(conn, msg, AVC_FORWARD, false, data);
}

static DBusMessage *control_previous(DBusConnection *conn, DBusMessage *msg,
								void *data)
{
	return key_pressed(conn, msg, AVC_BACKWARD, false, data);
}

static DBusMessage *control_fast_forward(DBusConnection *conn, DBusMessage *msg,
								void *data)
{
	return key_pressed(conn, msg, AVC_FAST_FORWARD, true, data);
}

static DBusMessage *control_rewind(DBusConnection *conn, DBusMessage *msg,
								void *data)
{
	return key_pressed(conn, msg, AVC_REWIND, true, data);
}

static gboolean control_property_get_connected(
					const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct control *control = data;
	dbus_bool_t value = (control->session != NULL);

	dbus_message_iter_append_basic(iter, DBUS_TYPE_BOOLEAN, &value);

	return TRUE;
}

static gboolean control_player_exists(const GDBusPropertyTable *property,
								void *data)
{
	struct control *control = data;

	return control->player != NULL;
}

static gboolean control_get_player(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct control *control = data;

	if (!control->player)
		return FALSE;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH,
							&control->player);

	return TRUE;
}

static const GDBusMethodTable control_methods[] = {
	{ GDBUS_DEPRECATED_METHOD("Play", NULL, NULL, control_play) },
	{ GDBUS_DEPRECATED_METHOD("Pause", NULL, NULL, control_pause) },
	{ GDBUS_DEPRECATED_METHOD("Stop", NULL, NULL, control_stop) },
	{ GDBUS_DEPRECATED_METHOD("Next", NULL, NULL, control_next) },
	{ GDBUS_DEPRECATED_METHOD("Previous", NULL, NULL, control_previous) },
	{ GDBUS_DEPRECATED_METHOD("VolumeUp", NULL, NULL, control_volume_up) },
	{ GDBUS_DEPRECATED_METHOD("VolumeDown", NULL, NULL,
							control_volume_down) },
	{ GDBUS_DEPRECATED_METHOD("FastForward", NULL, NULL,
							control_fast_forward) },
	{ GDBUS_DEPRECATED_METHOD("Rewind", NULL, NULL, control_rewind) },
	{ }
};

static const GDBusPropertyTable control_properties[] = {
	{ "Connected", "b", control_property_get_connected },
	{ "Player", "o", control_get_player, NULL, control_player_exists },
	{ }
};

static void path_unregister(void *data)
{
	struct control *control = data;

	DBG("Unregistered interface %s on path %s",  AUDIO_CONTROL_INTERFACE,
						device_get_path(control->dev));

	if (control->session)
		avctp_disconnect(control->session);

	avctp_remove_state_cb(control->avctp_id);

	if (control->target) {
		btd_service_set_user_data(control->target, NULL);
		btd_service_unref(control->target);
	}

	if (control->remote) {
		btd_service_set_user_data(control->remote, NULL);
		btd_service_unref(control->remote);
	}

	devices = g_slist_remove(devices, control);
	g_free(control);
}

void control_unregister(struct btd_service *service)
{
	struct btd_device *dev = btd_service_get_device(service);

	g_dbus_unregister_interface(btd_get_dbus_connection(),
						device_get_path(dev),
						AUDIO_CONTROL_INTERFACE);
}

static struct control *find_control(struct btd_device *dev)
{
	GSList *l;

	for (l = devices; l; l = l->next) {
		struct control *control = l->data;

		if (control->dev == dev)
			return control;
	}

	return NULL;
}

static struct control *control_init(struct btd_service *service)
{
	struct control *control;
	struct btd_device *dev = btd_service_get_device(service);

	control = find_control(dev);
	if (control != NULL)
		return control;

	control = g_new0(struct control, 1);

	if (!g_dbus_register_interface(btd_get_dbus_connection(),
					device_get_path(dev),
					AUDIO_CONTROL_INTERFACE,
					control_methods, NULL,
					control_properties, control,
					path_unregister)) {
		g_free(control);
		return NULL;
	}

	DBG("Registered interface %s on path %s", AUDIO_CONTROL_INTERFACE,
							device_get_path(dev));

	control->dev = dev;
	control->avctp_id = avctp_add_state_cb(dev, state_changed, control);
	devices = g_slist_prepend(devices, control);

	return control;
}

int control_init_target(struct btd_service *service)
{
	struct control *control;

	control = control_init(service);
	if (control == NULL)
		return -EINVAL;

	control->target = btd_service_ref(service);

	btd_service_set_user_data(service, control);

	return 0;
}

int control_init_remote(struct btd_service *service)
{
	struct control *control;

	control = control_init(service);
	if (control == NULL)
		return -EINVAL;

	control->remote = btd_service_ref(service);

	btd_service_set_user_data(service, control);

	return 0;
}

int control_set_player(struct btd_service *service, const char *path)
{
	struct control *control = btd_service_get_user_data(service);

	if (!control->session)
		return -ENOTCONN;

	if (g_strcmp0(control->player, path) == 0)
		return -EALREADY;

	control->player = path;

	g_dbus_emit_property_changed(btd_get_dbus_connection(),
					device_get_path(control->dev),
					AUDIO_CONTROL_INTERFACE, "Player");

	return 0;
}
