/* Copyright (c) 2013 The Chromium 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 "cras_bt_constants.h"
#include "cras_bt_manager.h"
#include "cras_bt_adapter.h"
#include "cras_bt_device.h"
#include "cras_bt_endpoint.h"
#include "cras_bt_player.h"
#include "cras_bt_profile.h"
#include "cras_bt_transport.h"
#include "utlist.h"


static void cras_bt_interface_added(DBusConnection *conn,
				    const char *object_path,
				    const char *interface_name,
				    DBusMessageIter *properties_array_iter)
{
	if (strcmp(interface_name, BLUEZ_INTERFACE_ADAPTER) == 0) {
		struct cras_bt_adapter *adapter;

		adapter = cras_bt_adapter_get(object_path);
		if (adapter) {
			cras_bt_adapter_update_properties(
				adapter, properties_array_iter, NULL);
		} else {
			adapter = cras_bt_adapter_create(object_path);
			if (adapter) {
				cras_bt_adapter_update_properties(
					adapter, properties_array_iter, NULL);
				cras_bt_register_endpoints(conn, adapter);
				cras_bt_register_player(conn, adapter);
				cras_bt_register_profiles(conn);

				syslog(LOG_INFO, "Bluetooth Adapter: %s added",
				       cras_bt_adapter_address(adapter));
			} else {
				syslog(LOG_WARNING,
				       "Failed to create Bluetooth Adapter: %s",
				       object_path);
			}
		}

	} else if (strcmp(interface_name, BLUEZ_INTERFACE_DEVICE) == 0) {
		struct cras_bt_device *device;

		device = cras_bt_device_get(object_path);
		if (device) {
			cras_bt_device_update_properties(
				device, properties_array_iter, NULL);
		} else {
			device = cras_bt_device_create(conn, object_path);
			if (device) {
				cras_bt_device_update_properties(
					device, properties_array_iter, NULL);

				syslog(LOG_INFO, "Bluetooth Device: %s added",
				       cras_bt_device_address(device));
			} else {
				syslog(LOG_WARNING,
				       "Failed to create Bluetooth Device: %s",
				       object_path);
			}
		}

	} else if (strcmp(interface_name,
			  BLUEZ_INTERFACE_MEDIA_TRANSPORT) == 0) {
		struct cras_bt_transport *transport;

		transport = cras_bt_transport_get(object_path);
		if (transport) {
			cras_bt_transport_update_properties(
				transport, properties_array_iter, NULL);
		} else {
			transport = cras_bt_transport_create(conn, object_path);
			if (transport) {
				cras_bt_transport_update_properties(
					transport, properties_array_iter, NULL);

				syslog(LOG_INFO,
				      "Bluetooth Transport: %s added",
				      cras_bt_transport_object_path(transport));
			} else {
				syslog(LOG_WARNING,
				       "Failed to create Bluetooth Transport: "
				       "%s",
				       object_path);
			}
		}

	}
}

static void cras_bt_interface_removed(DBusConnection *conn,
				      const char *object_path,
				      const char *interface_name)
{
	if (strcmp(interface_name, BLUEZ_INTERFACE_ADAPTER) == 0) {
		struct cras_bt_adapter *adapter;

		adapter = cras_bt_adapter_get(object_path);
		if (adapter) {
			syslog(LOG_INFO, "Bluetooth Adapter: %s removed",
			       cras_bt_adapter_address(adapter));
			cras_bt_adapter_destroy(adapter);
		}

	} else if (strcmp(interface_name, BLUEZ_INTERFACE_DEVICE) == 0) {
		struct cras_bt_device *device;

		device = cras_bt_device_get(object_path);
		if (device) {
			syslog(LOG_INFO, "Bluetooth Device: %s removed",
			       cras_bt_device_address(device));
			cras_bt_device_destroy(device);
		}

	} else if (strcmp(interface_name,
			  BLUEZ_INTERFACE_MEDIA_TRANSPORT) == 0) {
		struct cras_bt_transport *transport;

		transport = cras_bt_transport_get(object_path);
		if (transport) {
			syslog(LOG_INFO, "Bluetooth Transport: %s removed",
			       cras_bt_transport_object_path(transport));
			cras_bt_transport_destroy(transport);
		}

	}
}

static void cras_bt_update_properties(DBusConnection *conn,
				      const char *object_path,
				      const char *interface_name,
				      DBusMessageIter *properties_array_iter,
				      DBusMessageIter *invalidated_array_iter)
{
	if (strcmp(interface_name, BLUEZ_INTERFACE_ADAPTER) == 0) {
		struct cras_bt_adapter *adapter;

		adapter = cras_bt_adapter_get(object_path);
		if (adapter) {
			cras_bt_adapter_update_properties(
				adapter, properties_array_iter,
				invalidated_array_iter);
		}

	} else if (strcmp(interface_name, BLUEZ_INTERFACE_DEVICE) == 0) {
		struct cras_bt_device *device;

		device = cras_bt_device_get(object_path);
		if (device) {
			cras_bt_device_update_properties(
				device, properties_array_iter,
				invalidated_array_iter);
		}

	} else if (strcmp(interface_name,
			  BLUEZ_INTERFACE_MEDIA_TRANSPORT) == 0) {
		struct cras_bt_transport *transport;

		transport = cras_bt_transport_get(object_path);
		if (transport) {
			cras_bt_transport_update_properties(
				transport, properties_array_iter,
				invalidated_array_iter);
		}

	}
}

/* Destroys all bt related stuff. The reset functions must be called in
 * reverse order of the adapter -> device -> profile(s) hierarchy.
 */
static void cras_bt_reset()
{
	cras_bt_endpoint_reset();
	cras_bt_transport_reset();
	cras_bt_profile_reset();
	cras_bt_device_reset();
	cras_bt_adapter_reset();
}


static void cras_bt_on_get_managed_objects(DBusPendingCall *pending_call,
					   void *data)
{
	DBusConnection *conn = (DBusConnection *)data;
	DBusMessage *reply;
	DBusMessageIter message_iter, object_array_iter;

	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, "GetManagedObjects returned error: %s",
		       dbus_message_get_error_name(reply));
		dbus_message_unref(reply);
		return;
	}

	if (!dbus_message_has_signature(reply, "a{oa{sa{sv}}}")) {
		syslog(LOG_WARNING, "Bad GetManagedObjects reply received");
		dbus_message_unref(reply);
		return;
	}

	dbus_message_iter_init(reply, &message_iter);
	dbus_message_iter_recurse(&message_iter, &object_array_iter);

	while (dbus_message_iter_get_arg_type(&object_array_iter) !=
		       DBUS_TYPE_INVALID) {
		DBusMessageIter object_dict_iter, interface_array_iter;
		const char *object_path;

		dbus_message_iter_recurse(&object_array_iter,
					  &object_dict_iter);

		dbus_message_iter_get_basic(&object_dict_iter, &object_path);
		dbus_message_iter_next(&object_dict_iter);

		dbus_message_iter_recurse(&object_dict_iter,
					  &interface_array_iter);

		while (dbus_message_iter_get_arg_type(&interface_array_iter) !=
			       DBUS_TYPE_INVALID) {
			DBusMessageIter interface_dict_iter;
			DBusMessageIter properties_array_iter;
			const char *interface_name;

			dbus_message_iter_recurse(&interface_array_iter,
						  &interface_dict_iter);

			dbus_message_iter_get_basic(&interface_dict_iter,
						    &interface_name);
			dbus_message_iter_next(&interface_dict_iter);

			dbus_message_iter_recurse(&interface_dict_iter,
						  &properties_array_iter);

			cras_bt_interface_added(conn,
						object_path, interface_name,
						&properties_array_iter);

			dbus_message_iter_next(&interface_array_iter);
		}

		dbus_message_iter_next(&object_array_iter);
	}

	dbus_message_unref(reply);
}

static int cras_bt_get_managed_objects(DBusConnection *conn)
{
	DBusMessage *method_call;
	DBusPendingCall *pending_call;

	method_call = dbus_message_new_method_call(
		BLUEZ_SERVICE,
		"/",
		DBUS_INTERFACE_OBJECT_MANAGER,
		"GetManagedObjects");
	if (!method_call)
		return -ENOMEM;

	pending_call = NULL;
	if (!dbus_connection_send_with_reply(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_get_managed_objects,
					  conn, NULL)) {
		dbus_pending_call_cancel(pending_call);
		dbus_pending_call_unref(pending_call);
		return -ENOMEM;
	}

	return 0;
}


static DBusHandlerResult cras_bt_handle_name_owner_changed(DBusConnection *conn,
							   DBusMessage *message,
							   void *arg)
{
	DBusError dbus_error;
	const char *service_name, *old_owner, *new_owner;

	if (!dbus_message_is_signal(message, DBUS_INTERFACE_DBUS,
		    "NameOwnerChanged"))
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;

	dbus_error_init(&dbus_error);
	if (!dbus_message_get_args(message, &dbus_error,
				   DBUS_TYPE_STRING, &service_name,
				   DBUS_TYPE_STRING, &old_owner,
				   DBUS_TYPE_STRING, &new_owner,
				   DBUS_TYPE_INVALID)) {
		syslog(LOG_WARNING, "Bad NameOwnerChanged signal received: %s",
		       dbus_error.message);
		dbus_error_free(&dbus_error);
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
	}

	syslog(LOG_INFO, "Bluetooth daemon disconnected from the bus.");
	cras_bt_reset();

	if (strlen(new_owner) > 0)
		cras_bt_get_managed_objects(conn);

	return DBUS_HANDLER_RESULT_HANDLED;
}

static DBusHandlerResult cras_bt_handle_interfaces_added(DBusConnection *conn,
							 DBusMessage *message,
							 void *arg)
{
	DBusMessageIter message_iter, interface_array_iter;
	const char *object_path;

	if (!dbus_message_is_signal(message, DBUS_INTERFACE_OBJECT_MANAGER,
			    "InterfacesAdded"))
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;

	if (!dbus_message_has_signature(message, "oa{sa{sv}}")) {
		syslog(LOG_WARNING, "Bad InterfacesAdded signal received");
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
	}

	dbus_message_iter_init(message, &message_iter);

	dbus_message_iter_get_basic(&message_iter, &object_path);
	dbus_message_iter_next(&message_iter);

	dbus_message_iter_recurse(&message_iter, &interface_array_iter);

	while (dbus_message_iter_get_arg_type(&interface_array_iter) !=
		       DBUS_TYPE_INVALID) {
		DBusMessageIter interface_dict_iter, properties_array_iter;
		const char *interface_name;

		dbus_message_iter_recurse(&interface_array_iter,
					  &interface_dict_iter);

		dbus_message_iter_get_basic(&interface_dict_iter,
					    &interface_name);
		dbus_message_iter_next(&interface_dict_iter);

		dbus_message_iter_recurse(&interface_dict_iter,
					  &properties_array_iter);

		cras_bt_interface_added(conn, object_path, interface_name,
					&properties_array_iter);

		dbus_message_iter_next(&interface_array_iter);
	}

	return DBUS_HANDLER_RESULT_HANDLED;
}

static DBusHandlerResult cras_bt_handle_interfaces_removed(DBusConnection *conn,
							   DBusMessage *message,
							   void *arg)
{
	DBusMessageIter message_iter, interface_array_iter;
	const char *object_path;

	if (!dbus_message_is_signal(message, DBUS_INTERFACE_OBJECT_MANAGER,
			    "InterfacesRemoved"))
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;

	if (!dbus_message_has_signature(message, "oas")) {
		syslog(LOG_WARNING, "Bad InterfacesRemoved signal received");
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
	}

	dbus_message_iter_init(message, &message_iter);

	dbus_message_iter_get_basic(&message_iter, &object_path);
	dbus_message_iter_next(&message_iter);

	dbus_message_iter_recurse(&message_iter, &interface_array_iter);

	while (dbus_message_iter_get_arg_type(&interface_array_iter) !=
	       DBUS_TYPE_INVALID) {
		const char *interface_name;

		dbus_message_iter_get_basic(&interface_array_iter,
					    &interface_name);

		cras_bt_interface_removed(conn, object_path, interface_name);

		dbus_message_iter_next(&interface_array_iter);
	}

	return DBUS_HANDLER_RESULT_HANDLED;
}

static DBusHandlerResult cras_bt_handle_properties_changed(DBusConnection *conn,
							   DBusMessage *message,
							   void *arg)
{
	DBusMessageIter message_iter, properties_array_iter;
	DBusMessageIter invalidated_array_iter;
	const char *object_path, *interface_name;

	if (!dbus_message_is_signal(message, DBUS_INTERFACE_PROPERTIES,
				    "PropertiesChanged"))
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;

	if (!dbus_message_has_signature(message, "sa{sv}as")) {
		syslog(LOG_WARNING, "Bad PropertiesChanged signal received");
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
	}

	object_path = dbus_message_get_path(message);

	dbus_message_iter_init(message, &message_iter);

	dbus_message_iter_get_basic(&message_iter, &interface_name);
	dbus_message_iter_next(&message_iter);

	dbus_message_iter_recurse(&message_iter, &properties_array_iter);
	dbus_message_iter_next(&message_iter);

	dbus_message_iter_recurse(&message_iter, &invalidated_array_iter);

	cras_bt_update_properties(conn, object_path, interface_name,
				  &properties_array_iter,
				  &invalidated_array_iter);

	return DBUS_HANDLER_RESULT_HANDLED;
}


void cras_bt_start(DBusConnection *conn)
{
	DBusError dbus_error;

	dbus_error_init(&dbus_error);

	/* Inform the bus daemon which signals we wish to receive. */
	dbus_bus_add_match(conn,
			   "type='signal',"
			   "sender='" DBUS_SERVICE_DBUS "',"
			   "interface='" DBUS_INTERFACE_DBUS "',"
			   "member='NameOwnerChanged',"
			   "arg0='" BLUEZ_SERVICE "'",
			   &dbus_error);
	if (dbus_error_is_set(&dbus_error))
		goto add_match_error;

	dbus_bus_add_match(conn,
			   "type='signal',"
			   "sender='" BLUEZ_SERVICE "',"
			   "interface='" DBUS_INTERFACE_OBJECT_MANAGER "',"
			   "member='InterfacesAdded'",
			   &dbus_error);
	if (dbus_error_is_set(&dbus_error))
		goto add_match_error;

	dbus_bus_add_match(conn,
			   "type='signal',"
			   "sender='" BLUEZ_SERVICE "',"
			   "interface='" DBUS_INTERFACE_OBJECT_MANAGER "',"
			   "member='InterfacesRemoved'",
			   &dbus_error);
	if (dbus_error_is_set(&dbus_error))
		goto add_match_error;

	dbus_bus_add_match(conn,
			   "type='signal',"
			   "sender='" BLUEZ_SERVICE "',"
			   "interface='" DBUS_INTERFACE_PROPERTIES "',"
			   "member='PropertiesChanged'",
			   &dbus_error);
	if (dbus_error_is_set(&dbus_error))
		goto add_match_error;

	/* Install filter functions to handle the signals we receive. */
	if (!dbus_connection_add_filter(conn, cras_bt_handle_name_owner_changed,
					NULL, NULL))
		goto add_filter_error;

	if (!dbus_connection_add_filter(conn, cras_bt_handle_interfaces_added,
					NULL, NULL))
		goto add_filter_error;

	if (!dbus_connection_add_filter(conn, cras_bt_handle_interfaces_removed,
					NULL, NULL))
		goto add_filter_error;

	if (!dbus_connection_add_filter(conn, cras_bt_handle_properties_changed,
					NULL, NULL))
		goto add_filter_error;

	cras_bt_get_managed_objects(conn);
	return;

add_match_error:
	syslog(LOG_WARNING, "Couldn't setup Bluetooth device monitoring: %s",
	       dbus_error.message);
	dbus_error_free(&dbus_error);
	cras_bt_stop(conn);
	return;

add_filter_error:
	syslog(LOG_WARNING, "Couldn't setup Bluetooth device monitoring: %s",
	       strerror(ENOMEM));
	cras_bt_stop(conn);
	return;
}

void cras_bt_stop(DBusConnection *conn)
{
	cras_bt_reset();

	dbus_bus_remove_match(conn,
			      "type='signal',"
			      "sender='" DBUS_SERVICE_DBUS "',"
			      "interface='" DBUS_INTERFACE_DBUS "',"
			      "member='NameOwnerChanged',"
			      "arg0='" BLUEZ_SERVICE "'",
			      NULL);
	dbus_bus_remove_match(conn,
			      "type='signal',"
			      "sender='" BLUEZ_SERVICE "',"
			      "interface='" DBUS_INTERFACE_OBJECT_MANAGER "',"
			      "member='InterfacesAdded'",
			      NULL);
	dbus_bus_remove_match(conn,
			      "type='signal',"
			      "sender='" BLUEZ_SERVICE "',"
			      "interface='" DBUS_INTERFACE_OBJECT_MANAGER "',"
			      "member='InterfacesRemoved'",
			      NULL);
	dbus_bus_remove_match(conn,
			      "type='signal',"
			      "sender='" BLUEZ_SERVICE "',"
			      "interface='" DBUS_INTERFACE_PROPERTIES "',"
			      "member='PropertiesChanged'",
			      NULL);

	dbus_connection_remove_filter(conn, cras_bt_handle_name_owner_changed,
				      NULL);
	dbus_connection_remove_filter(conn, cras_bt_handle_interfaces_added,
				      NULL);
	dbus_connection_remove_filter(conn, cras_bt_handle_interfaces_removed,
				      NULL);
	dbus_connection_remove_filter(conn, cras_bt_handle_properties_changed,
				      NULL);
}