/* Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include <dbus/dbus.h>

#include <errno.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>

#include "cras_bt_device.h"
#include "cras_bt_endpoint.h"
#include "cras_bt_transport.h"
#include "cras_bt_constants.h"
#include "utlist.h"


struct cras_bt_transport {
	DBusConnection *conn;
	char *object_path;
	struct cras_bt_device *device;
	enum cras_bt_device_profile profile;
	int codec;
	void *configuration;
	int configuration_len;
	enum cras_bt_transport_state state;
	int fd;
	uint16_t read_mtu;
	uint16_t write_mtu;
	int volume;

	struct cras_bt_endpoint *endpoint;
	struct cras_bt_transport *prev, *next;
};

static struct cras_bt_transport *transports;

struct cras_bt_transport *cras_bt_transport_create(DBusConnection *conn,
						   const char *object_path)
{
	struct cras_bt_transport *transport;

	transport = calloc(1, sizeof(*transport));
	if (transport == NULL)
		return NULL;

	transport->object_path = strdup(object_path);
	if (transport->object_path == NULL) {
		free(transport);
		return NULL;
	}

	transport->conn = conn;
	dbus_connection_ref(transport->conn);

	transport->fd = -1;
	transport->volume = -1;

	DL_APPEND(transports, transport);

	return transport;
}

void cras_bt_transport_set_endpoint(struct cras_bt_transport *transport,
				    struct cras_bt_endpoint *endpoint) {
	transport->endpoint = endpoint;
}

void cras_bt_transport_destroy(struct cras_bt_transport *transport)
{
	DL_DELETE(transports, transport);

	dbus_connection_unref(transport->conn);

	if (transport->fd >= 0)
		close(transport->fd);

	free(transport->object_path);
	free(transport->configuration);
	free(transport);
}

void cras_bt_transport_reset()
{
	while (transports) {
		syslog(LOG_INFO, "Bluetooth Transport: %s removed",
		       transports->object_path);
		cras_bt_transport_destroy(transports);
	}
}


struct cras_bt_transport *cras_bt_transport_get(const char *object_path)
{
	struct cras_bt_transport *transport;

	DL_FOREACH(transports, transport) {
		if (strcmp(transport->object_path, object_path) == 0)
			return transport;
	}

	return NULL;
}

size_t cras_bt_transport_get_list(
	struct cras_bt_transport ***transport_list_out)
{
	struct cras_bt_transport *transport;
	struct cras_bt_transport **transport_list = NULL;
	size_t num_transports = 0;

	DL_FOREACH(transports, transport) {
		struct cras_bt_transport **tmp;

		tmp = realloc(transport_list,
			      sizeof(transport_list[0]) * (num_transports + 1));
		if (!tmp) {
			free(transport_list);
			return -ENOMEM;
		}

		transport_list = tmp;
		transport_list[num_transports++] = transport;
	}

	*transport_list_out = transport_list;
	return num_transports;
}

const char *cras_bt_transport_object_path(
	const struct cras_bt_transport *transport)
{
	return transport->object_path;
}

struct cras_bt_device *cras_bt_transport_device(
	const struct cras_bt_transport *transport)
{
	return transport->device;
}

enum cras_bt_device_profile cras_bt_transport_profile(
	const struct cras_bt_transport *transport)
{
	return transport->profile;
}

int cras_bt_transport_configuration(const struct cras_bt_transport *transport,
				    void *configuration, int len)
{
	if (len < transport->configuration_len)
		return -ENOSPC;

	memcpy(configuration, transport->configuration,
	       transport->configuration_len);

	return 0;
}

enum cras_bt_transport_state cras_bt_transport_state(
	const struct cras_bt_transport *transport)
{
	return transport->state;
}

int cras_bt_transport_fd(const struct cras_bt_transport *transport)
{
	return transport->fd;
}

uint16_t cras_bt_transport_write_mtu(const struct cras_bt_transport *transport)
{
	return transport->write_mtu;
}

static enum cras_bt_transport_state cras_bt_transport_state_from_string(
	const char *value)
{
	if (strcmp("idle", value) == 0)
		return CRAS_BT_TRANSPORT_STATE_IDLE;
	else if (strcmp("pending", value) == 0)
		return CRAS_BT_TRANSPORT_STATE_PENDING;
	else if (strcmp("active", value) == 0)
		return CRAS_BT_TRANSPORT_STATE_ACTIVE;
	else
		return CRAS_BT_TRANSPORT_STATE_IDLE;
}

static void cras_bt_transport_state_changed(struct cras_bt_transport *transport)
{
	if (transport->endpoint &&
	    transport->endpoint->transport_state_changed)
		transport->endpoint->transport_state_changed(
				transport->endpoint,
				transport);
}

/* Updates bt_device when certain transport property has changed. */
static void cras_bt_transport_update_device(struct cras_bt_transport *transport)
{
	if (!transport->device)
		return;

	/* When the transport has non-negaive volume, it means the remote
	 * BT audio devices supports AVRCP absolute volume. Set the flag in bt
	 * device to use hardware volume. Also map the volume value from 0-127
	 * to 0-100.
	 */
	if (transport->volume != -1) {
		cras_bt_device_set_use_hardware_volume(transport->device, 1);
		cras_bt_device_update_hardware_volume(
				transport->device,
				transport->volume * 100 / 127);
	}
}

void cras_bt_transport_update_properties(
	struct cras_bt_transport *transport,
	DBusMessageIter *properties_array_iter,
	DBusMessageIter *invalidated_array_iter)
{
	while (dbus_message_iter_get_arg_type(properties_array_iter) !=
	       DBUS_TYPE_INVALID) {
		DBusMessageIter properties_dict_iter, variant_iter;
		const char *key;
		int type;

		dbus_message_iter_recurse(properties_array_iter,
					  &properties_dict_iter);

		dbus_message_iter_get_basic(&properties_dict_iter, &key);
		dbus_message_iter_next(&properties_dict_iter);

		dbus_message_iter_recurse(&properties_dict_iter, &variant_iter);
		type = dbus_message_iter_get_arg_type(&variant_iter);

		if (type == DBUS_TYPE_STRING) {
			const char *value;

			dbus_message_iter_get_basic(&variant_iter, &value);

			if (strcmp(key, "UUID") == 0) {
				transport->profile =
					cras_bt_device_profile_from_uuid(value);

			} else if (strcmp(key, "State") == 0) {
				enum cras_bt_transport_state old_state =
					transport->state;
				transport->state =
					cras_bt_transport_state_from_string(
						value);
				if (transport->state != old_state)
					cras_bt_transport_state_changed(
						transport);
			}

		} else if (type == DBUS_TYPE_BYTE) {
			int value;

			dbus_message_iter_get_basic(&variant_iter, &value);

			if (strcmp(key, "Codec") == 0)
				transport->codec = value;
		} else if (type == DBUS_TYPE_OBJECT_PATH) {
			const char *obj_path;

			/* Property: object Device [readonly] */
			dbus_message_iter_get_basic(&variant_iter, &obj_path);
			transport->device = cras_bt_device_get(obj_path);
			if (!transport->device) {
				syslog(LOG_ERR, "Device %s not found at update"
				       "transport properties",
				       obj_path);
				transport->device =
					cras_bt_device_create(transport->conn,
							      obj_path);
				cras_bt_transport_update_device(transport);
			}
		} else if (strcmp(
				dbus_message_iter_get_signature(&variant_iter),
				"ay") == 0 &&
			   strcmp(key, "Configuration") == 0) {
			DBusMessageIter value_iter;
			char *value;
			int len;

			dbus_message_iter_recurse(&variant_iter, &value_iter);
			dbus_message_iter_get_fixed_array(&value_iter,
							  &value, &len);

			free(transport->configuration);
			transport->configuration_len = 0;

			transport->configuration = malloc(len);
			if (transport->configuration) {
				memcpy(transport->configuration, value, len);
				transport->configuration_len = len;
			}

		} else if (strcmp(key, "Volume") == 0) {
			uint16_t volume;

			dbus_message_iter_get_basic(&variant_iter, &volume);
			transport->volume = volume;
			cras_bt_transport_update_device(transport);
		}

		dbus_message_iter_next(properties_array_iter);
	}

	while (invalidated_array_iter &&
	       dbus_message_iter_get_arg_type(invalidated_array_iter) !=
	       DBUS_TYPE_INVALID) {
		const char *key;

		dbus_message_iter_get_basic(invalidated_array_iter, &key);

		if (strcmp(key, "Device") == 0) {
			transport->device = NULL;
		} else if (strcmp(key, "UUID") == 0) {
			transport->profile = 0;
		} else if (strcmp(key, "State") == 0) {
			transport->state = CRAS_BT_TRANSPORT_STATE_IDLE;
		} else if (strcmp(key, "Codec") == 0) {
			transport->codec = 0;
		} else if (strcmp(key, "Configuration") == 0) {
			free(transport->configuration);
			transport->configuration = NULL;
			transport->configuration_len = 0;
		}

		dbus_message_iter_next(invalidated_array_iter);
	}
}

static void on_transport_volume_set(DBusPendingCall *pending_call, void *data)
{
	DBusMessage *reply;

	reply = dbus_pending_call_steal_reply(pending_call);
	dbus_pending_call_unref(pending_call);

	if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR)
		syslog(LOG_ERR, "Set absolute volume returned error: %s",
		       dbus_message_get_error_name(reply));
	dbus_message_unref(reply);
}

int cras_bt_transport_set_volume(struct cras_bt_transport *transport,
				 uint16_t volume)
{
	const char *key = "Volume";
	const char *interface = BLUEZ_INTERFACE_MEDIA_TRANSPORT;
	DBusMessage *method_call;
	DBusMessageIter message_iter, variant;
	DBusPendingCall *pending_call;

	method_call = dbus_message_new_method_call(
		BLUEZ_SERVICE,
		transport->object_path,
		DBUS_INTERFACE_PROPERTIES,
		"Set");
	if (!method_call)
		return -ENOMEM;

	dbus_message_iter_init_append(method_call, &message_iter);

	dbus_message_iter_append_basic(&message_iter, DBUS_TYPE_STRING,
				       &interface);
	dbus_message_iter_append_basic(&message_iter, DBUS_TYPE_STRING, &key);

	dbus_message_iter_open_container(&message_iter, DBUS_TYPE_VARIANT,
					 DBUS_TYPE_UINT16_AS_STRING, &variant);
	dbus_message_iter_append_basic(&variant, DBUS_TYPE_UINT16, &volume);
	dbus_message_iter_close_container(&message_iter, &variant);

	if (!dbus_connection_send_with_reply(transport->conn, method_call,
					     &pending_call,
					     DBUS_TIMEOUT_USE_DEFAULT)) {
		dbus_message_unref(method_call);
		return -ENOMEM;
	}

	dbus_message_unref(method_call);
	if (!pending_call)
		return -EIO;

	if (!dbus_pending_call_set_notify(pending_call,
					  on_transport_volume_set,
					  NULL, NULL)) {
		dbus_pending_call_cancel(pending_call);
		dbus_pending_call_unref(pending_call);
		return -ENOMEM;
	}

	return 0;
}

int cras_bt_transport_acquire(struct cras_bt_transport *transport)
{
	DBusMessage *method_call, *reply;
	DBusError dbus_error;

	if (transport->fd >= 0)
		return 0;

	method_call = dbus_message_new_method_call(
		BLUEZ_SERVICE,
		transport->object_path,
		BLUEZ_INTERFACE_MEDIA_TRANSPORT,
		"Acquire");
	if (!method_call)
		return -ENOMEM;

	dbus_error_init(&dbus_error);

	reply = dbus_connection_send_with_reply_and_block(
		transport->conn,
		method_call,
		DBUS_TIMEOUT_USE_DEFAULT,
		&dbus_error);
	if (!reply) {
		syslog(LOG_ERR, "Failed to acquire transport %s: %s",
		       transport->object_path, dbus_error.message);
		dbus_error_free(&dbus_error);
		dbus_message_unref(method_call);
		return -EIO;
	}

	dbus_message_unref(method_call);

	if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) {
		syslog(LOG_ERR, "Acquire returned error: %s",
		       dbus_message_get_error_name(reply));
		dbus_message_unref(reply);
		return -EIO;
	}

	if (!dbus_message_get_args(reply, &dbus_error,
				   DBUS_TYPE_UNIX_FD, &(transport->fd),
				   DBUS_TYPE_UINT16, &(transport->read_mtu),
				   DBUS_TYPE_UINT16, &(transport->write_mtu),
				   DBUS_TYPE_INVALID)) {
		syslog(LOG_ERR, "Bad Acquire reply received: %s",
		       dbus_error.message);
		dbus_error_free(&dbus_error);
		dbus_message_unref(reply);
		return -EINVAL;
	}

	dbus_message_unref(reply);
	return 0;
}

int cras_bt_transport_try_acquire(struct cras_bt_transport *transport)
{
	DBusMessage *method_call, *reply;
	DBusError dbus_error;
	int fd, read_mtu, write_mtu;

	method_call = dbus_message_new_method_call(
			BLUEZ_SERVICE,
			transport->object_path,
			BLUEZ_INTERFACE_MEDIA_TRANSPORT,
			"TryAcquire");
	if (!method_call)
		return -ENOMEM;

	dbus_error_init(&dbus_error);

	reply = dbus_connection_send_with_reply_and_block(
		transport->conn,
		method_call,
		DBUS_TIMEOUT_USE_DEFAULT,
		&dbus_error);
	if (!reply) {
		syslog(LOG_ERR, "Failed to try acquire transport %s: %s",
		       transport->object_path, dbus_error.message);
		dbus_error_free(&dbus_error);
		dbus_message_unref(method_call);
		return -EIO;
	}

	dbus_message_unref(method_call);

	if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) {
		syslog(LOG_ERR, "TryAcquire returned error: %s",
		       dbus_message_get_error_name(reply));
		dbus_message_unref(reply);
		return -EIO;
	}

	if (!dbus_message_get_args(reply, &dbus_error,
				   DBUS_TYPE_UNIX_FD, &fd,
				   DBUS_TYPE_UINT16, &read_mtu,
				   DBUS_TYPE_UINT16, &write_mtu,
				   DBUS_TYPE_INVALID)) {
		syslog(LOG_ERR, "Bad TryAcquire reply received: %s",
		       dbus_error.message);
		dbus_error_free(&dbus_error);
		dbus_message_unref(reply);
		return -EINVAL;
	}

	/* Done TryAcquired the transport so it won't be released in bluez,
	 * no need for the new file descriptor so close it. */
	if (transport->fd != fd)
		close(fd);

	dbus_message_unref(reply);
	return 0;
}

/* Callback to trigger when transport release completed. */
static void cras_bt_on_transport_release(DBusPendingCall *pending_call,
					 void *data)
{
	DBusMessage *reply;

	reply = dbus_pending_call_steal_reply(pending_call);
	dbus_pending_call_unref(pending_call);

	if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) {
		syslog(LOG_WARNING, "Release transport returned error: %s",
		       dbus_message_get_error_name(reply));
		dbus_message_unref(reply);
		return;
	}

	dbus_message_unref(reply);
}

int cras_bt_transport_release(struct cras_bt_transport *transport,
			      unsigned int blocking)
{
	DBusMessage *method_call, *reply;
	DBusPendingCall *pending_call;
	DBusError dbus_error;

	if (transport->fd < 0)
		return 0;

	/* Close the transport on our end no matter whether or not the server
	 * gives us an error.
	 */
	close(transport->fd);
	transport->fd = -1;

	method_call = dbus_message_new_method_call(
		BLUEZ_SERVICE,
		transport->object_path,
		BLUEZ_INTERFACE_MEDIA_TRANSPORT,
		"Release");
	if (!method_call)
		return -ENOMEM;

	if (blocking) {
		dbus_error_init(&dbus_error);

		reply = dbus_connection_send_with_reply_and_block(
			transport->conn,
			method_call,
			DBUS_TIMEOUT_USE_DEFAULT,
			&dbus_error);
		if (!reply) {
			syslog(LOG_ERR, "Failed to release transport %s: %s",
			       transport->object_path, dbus_error.message);
			dbus_error_free(&dbus_error);
			dbus_message_unref(method_call);
			return -EIO;
		}

		dbus_message_unref(method_call);

		if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) {
			syslog(LOG_ERR, "Release returned error: %s",
			       dbus_message_get_error_name(reply));
			dbus_message_unref(reply);
			return -EIO;
		}

		dbus_message_unref(reply);
	} else {
		if (!dbus_connection_send_with_reply(
				transport->conn,
				method_call,
				&pending_call,
				DBUS_TIMEOUT_USE_DEFAULT)) {
			dbus_message_unref(method_call);
			return -ENOMEM;
		}

		dbus_message_unref(method_call);
		if (!pending_call)
			return -EIO;

		if (!dbus_pending_call_set_notify(pending_call,
						  cras_bt_on_transport_release,
						  transport, NULL)) {
			dbus_pending_call_cancel(pending_call);
			dbus_pending_call_unref(pending_call);
			return -ENOMEM;
		}
	}
	return 0;
}