/*
 *
 *  BlueZ - Bluetooth protocol stack for Linux
 *
 *  Copyright (C) 2006-2008  Nokia Corporation
 *  Copyright (C) 2004-2008  Marcel Holtmann <marcel@holtmann.org>
 *
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

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

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/ioctl.h>

#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>
#include <bluetooth/sdp.h>

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

#include "logging.h"

#include "hcid.h"
#include "adapter.h"
#include "device.h"
#include "agent.h"

#define REQUEST_TIMEOUT (60 * 1000)		/* 60 seconds */
#define AGENT_TIMEOUT (10 * 60 * 1000)		/* 10 minutes */

typedef enum {
	AGENT_REQUEST_PASSKEY,
	AGENT_REQUEST_CONFIRMATION,
	AGENT_REQUEST_PINCODE,
	AGENT_REQUEST_AUTHORIZE,
	AGENT_REQUEST_CONFIRM_MODE
} agent_request_type_t;

struct agent {
	struct btd_adapter *adapter;
	char *name;
	char *path;
	uint8_t capability;
	struct agent_request *request;
	int exited;
	agent_remove_cb remove_cb;
	void *remove_cb_data;
	guint listener_id;
};

struct agent_request {
	agent_request_type_t type;
	struct agent *agent;
	DBusMessage *msg;
	DBusPendingCall *call;
	void *cb;
	void *user_data;
};

static DBusConnection *connection = NULL;

static int request_fallback(struct agent_request *req,
				DBusPendingCallNotifyFunction function);

static void agent_release(struct agent *agent)
{
	DBusMessage *message;

	debug("Releasing agent %s, %s", agent->name, agent->path);

	if (agent->request)
		agent_cancel(agent);

	message = dbus_message_new_method_call(agent->name, agent->path,
			"org.bluez.Agent", "Release");
	if (message == NULL) {
		error("Couldn't allocate D-Bus message");
		return;
	}

	dbus_message_set_no_reply(message, TRUE);

	g_dbus_send_message(connection, message);
}

static int send_cancel_request(struct agent_request *req)
{
	DBusMessage *message;

	message = dbus_message_new_method_call(req->agent->name, req->agent->path,
						"org.bluez.Agent", "Cancel");
	if (message == NULL) {
		error("Couldn't allocate D-Bus message");
		return -ENOMEM;
	}

	dbus_message_set_no_reply(message, TRUE);

	g_dbus_send_message(connection, message);

	return 0;
}

static void agent_request_free(struct agent_request *req)
{
	if (req->msg)
		dbus_message_unref(req->msg);
	if (req->call)
		dbus_pending_call_unref(req->call);
	if (req->agent && req->agent->request)
		req->agent->request = NULL;
	g_free(req);
}

static void agent_exited(DBusConnection *conn, void *user_data)
{
	struct agent *agent = user_data;

	debug("Agent exited without calling Unregister");

	agent_destroy(agent, TRUE);
}

static void agent_free(struct agent *agent)
{
	if (!agent)
		return;

	if (agent->remove_cb)
		agent->remove_cb(agent, agent->remove_cb_data);

	if (agent->request) {
		DBusError err;
		agent_pincode_cb pincode_cb;
		agent_cb cb;

		dbus_error_init(&err);
		dbus_set_error_const(&err, "org.bluez.Error.Failed", "Canceled");

		switch (agent->request->type) {
		case AGENT_REQUEST_PINCODE:
			pincode_cb = agent->request->cb;
			pincode_cb(agent, &err, NULL, agent->request->user_data);
			break;
		default:
			cb = agent->request->cb;
			cb(agent, &err, agent->request->user_data);
		}

		dbus_error_free(&err);

		agent_cancel(agent);
	}

	if (!agent->exited) {
		g_dbus_remove_watch(connection, agent->listener_id);
		agent_release(agent);
	}

	g_free(agent->name);
	g_free(agent->path);

	g_free(agent);
}

struct agent *agent_create(struct btd_adapter *adapter, const char *name,
				const char *path, uint8_t capability,
				agent_remove_cb cb, void *remove_cb_data)
{
	struct agent *agent, *adapter_agent;

	adapter_agent = adapter_get_agent(adapter);
	if (adapter_agent && g_str_equal(adapter_agent->name, name))
		return NULL;

	agent = g_new0(struct agent, 1);

	agent->adapter = adapter;
	agent->name = g_strdup(name);
	agent->path = g_strdup(path);
	agent->capability = capability;
	agent->remove_cb = cb;
	agent->remove_cb_data = remove_cb_data;

	agent->listener_id = g_dbus_add_disconnect_watch(connection, name,
							agent_exited, agent,
							NULL);

	return agent;
}

int agent_destroy(struct agent *agent, gboolean exited)
{
	if (!agent)
		return 0;

	agent->exited = exited;
	agent_free(agent);
	return 0;
}

static struct agent_request *agent_request_new(struct agent *agent,
						agent_request_type_t type,
						void *cb,
						void *user_data)
{
	struct agent_request *req;

	req = g_new0(struct agent_request, 1);

	req->agent = agent;
	req->type = type;
	req->cb = cb;
	req->user_data = user_data;

	return req;
}

int agent_cancel(struct agent *agent)
{
	if (!agent->request)
		return -EINVAL;

	if (agent->request->call)
		dbus_pending_call_cancel(agent->request->call);

	if (!agent->exited)
		send_cancel_request(agent->request);

	agent_request_free(agent->request);
	agent->request = NULL;

	return 0;
}

static void simple_agent_reply(DBusPendingCall *call, void *user_data)
{
	struct agent_request *req = user_data;
	struct agent *agent = req->agent;
	DBusMessage *message;
	DBusError err;
	agent_cb cb = req->cb;

	/* steal_reply will always return non-NULL since the callback
	 * is only called after a reply has been received */
	message = dbus_pending_call_steal_reply(call);

	dbus_error_init(&err);
	if (dbus_set_error_from_message(&err, message)) {
		if ((g_str_equal(DBUS_ERROR_UNKNOWN_METHOD, err.name) ||
				g_str_equal(DBUS_ERROR_NO_REPLY, err.name)) &&
				request_fallback(req, simple_agent_reply) == 0) {
			dbus_error_free(&err);
			return;
		}

		error("Agent replied with an error: %s, %s",
				err.name, err.message);

		cb(agent, &err, req->user_data);

		if (dbus_error_has_name(&err, DBUS_ERROR_NO_REPLY)) {
			agent_cancel(agent);
			dbus_message_unref(message);
			dbus_error_free(&err);
			return;
		}

		dbus_error_free(&err);
		goto done;
	}

	dbus_error_init(&err);
	if (!dbus_message_get_args(message, &err, DBUS_TYPE_INVALID)) {
		error("Wrong reply signature: %s", err.message);
		cb(agent, &err, req->user_data);
		dbus_error_free(&err);
		goto done;
	}

	cb(agent, NULL, req->user_data);
done:
	dbus_message_unref(message);

	agent->request = NULL;
	agent_request_free(req);
}

static int agent_call_authorize(struct agent_request *req,
				const char *device_path,
				const char *uuid)
{
	struct agent *agent = req->agent;

	req->msg = dbus_message_new_method_call(agent->name, agent->path,
				"org.bluez.Agent", "Authorize");
	if (!req->msg) {
		error("Couldn't allocate D-Bus message");
		return -ENOMEM;
	}

	dbus_message_append_args(req->msg,
				DBUS_TYPE_OBJECT_PATH, &device_path,
				DBUS_TYPE_STRING, &uuid,
				DBUS_TYPE_INVALID);

	if (dbus_connection_send_with_reply(connection, req->msg,
					&req->call, REQUEST_TIMEOUT) == FALSE) {
		error("D-Bus send failed");
		return -EIO;
	}

	dbus_pending_call_set_notify(req->call, simple_agent_reply, req, NULL);
	return 0;
}

int agent_authorize(struct agent *agent,
			const char *path,
			const char *uuid,
			agent_cb cb,
			void *user_data)
{
	struct agent_request *req;
	int err;

	if (agent->request)
		return -EBUSY;

	req = agent_request_new(agent, AGENT_REQUEST_AUTHORIZE, cb, user_data);

	err = agent_call_authorize(req, path, uuid);
	if (err < 0) {
		agent_request_free(req);
		return DBUS_HANDLER_RESULT_NEED_MEMORY;
	}

	agent->request = req;

	debug("authorize request was sent for %s", path);

	return 0;
}

static void pincode_reply(DBusPendingCall *call, void *user_data)
{
	struct agent_request *req = user_data;
	struct agent *agent = req->agent;
	struct btd_adapter *adapter = agent->adapter;
	agent_pincode_cb cb = req->cb;
	DBusMessage *message;
	DBusError err;
	bdaddr_t sba;
	size_t len;
	char *pin;

	adapter_get_address(adapter, &sba);

	/* steal_reply will always return non-NULL since the callback
	 * is only called after a reply has been received */
	message = dbus_pending_call_steal_reply(call);

	dbus_error_init(&err);
	if (dbus_set_error_from_message(&err, message)) {
		if ((g_str_equal(DBUS_ERROR_UNKNOWN_METHOD, err.name) ||
				g_str_equal(DBUS_ERROR_NO_REPLY, err.name)) &&
				request_fallback(req, pincode_reply) == 0) {
			dbus_error_free(&err);
			return;
		}

		error("Agent replied with an error: %s, %s",
				err.name, err.message);

		cb(agent, &err, NULL, req->user_data);
		dbus_error_free(&err);
		goto done;
	}

	dbus_error_init(&err);
	if (!dbus_message_get_args(message, &err,
				DBUS_TYPE_STRING, &pin,
				DBUS_TYPE_INVALID)) {
		error("Wrong passkey reply signature: %s", err.message);
		cb(agent, &err, NULL, req->user_data);
		dbus_error_free(&err);
		goto done;
	}

	len = strlen(pin);

	dbus_error_init(&err);
	if (len > 16 || len < 1) {
		error("Invalid passkey length from handler");
		dbus_set_error_const(&err, "org.bluez.Error.InvalidArgs",
					"Invalid passkey length");
		cb(agent, &err, NULL, req->user_data);
		dbus_error_free(&err);
		goto done;
	}

	set_pin_length(&sba, len);

	cb(agent, NULL, pin, req->user_data);

done:
	if (message)
		dbus_message_unref(message);

	dbus_pending_call_cancel(req->call);
	agent->request = NULL;
	agent_request_free(req);
}

static int pincode_request_new(struct agent_request *req, const char *device_path,
				dbus_bool_t numeric)
{
	struct agent *agent = req->agent;

	req->msg = dbus_message_new_method_call(agent->name, agent->path,
					"org.bluez.Agent", "RequestPinCode");
	if (req->msg == NULL) {
		error("Couldn't allocate D-Bus message");
		return -ENOMEM;
	}

	dbus_message_append_args(req->msg, DBUS_TYPE_OBJECT_PATH, &device_path,
					DBUS_TYPE_INVALID);

	if (dbus_connection_send_with_reply(connection, req->msg,
					&req->call, REQUEST_TIMEOUT) == FALSE) {
		error("D-Bus send failed");
		return -EIO;
	}

	dbus_pending_call_set_notify(req->call, pincode_reply, req, NULL);
	return 0;
}

int agent_request_pincode(struct agent *agent, struct btd_device *device,
				agent_pincode_cb cb, void *user_data)
{
	struct agent_request *req;
	const gchar *dev_path = device_get_path(device);
	int err;

	if (agent->request)
		return -EBUSY;

	req = agent_request_new(agent, AGENT_REQUEST_PINCODE, cb, user_data);

	err = pincode_request_new(req, dev_path, FALSE);
	if (err < 0)
		goto failed;

	agent->request = req;

	return 0;

failed:
	g_free(req);
	return err;
}

static int confirm_mode_change_request_new(struct agent_request *req,
						const char *mode)
{
	struct agent *agent = req->agent;

	req->msg = dbus_message_new_method_call(agent->name, agent->path,
				"org.bluez.Agent", "ConfirmModeChange");
	if (req->msg == NULL) {
		error("Couldn't allocate D-Bus message");
		return -ENOMEM;
	}

	dbus_message_append_args(req->msg,
				DBUS_TYPE_STRING, &mode,
				DBUS_TYPE_INVALID);

	if (dbus_connection_send_with_reply(connection, req->msg,
					&req->call, REQUEST_TIMEOUT) == FALSE) {
		error("D-Bus send failed");
		return -EIO;
	}

	dbus_pending_call_set_notify(req->call, simple_agent_reply, req, NULL);
	return 0;
}

int agent_confirm_mode_change(struct agent *agent, const char *new_mode,
				agent_cb cb, void *user_data)
{
	struct agent_request *req;
	int err;

	if (agent->request)
		return -EBUSY;

	debug("Calling Agent.ConfirmModeChange: name=%s, path=%s, mode=%s",
			agent->name, agent->path, new_mode);

	req = agent_request_new(agent, AGENT_REQUEST_CONFIRM_MODE,
				cb, user_data);

	err = confirm_mode_change_request_new(req, new_mode);
	if (err < 0)
		goto failed;

	agent->request = req;

	return 0;

failed:
	agent_request_free(req);
	return err;
}

static void passkey_reply(DBusPendingCall *call, void *user_data)
{
	struct agent_request *req = user_data;
	struct agent *agent = req->agent;
	agent_passkey_cb cb = req->cb;
	DBusMessage *message;
	DBusError err;
	uint32_t passkey;

	/* steal_reply will always return non-NULL since the callback
	 * is only called after a reply has been received */
	message = dbus_pending_call_steal_reply(call);

	dbus_error_init(&err);
	if (dbus_set_error_from_message(&err, message)) {
		if ((g_str_equal(DBUS_ERROR_UNKNOWN_METHOD, err.name) ||
				g_str_equal(DBUS_ERROR_NO_REPLY, err.name)) &&
				request_fallback(req, passkey_reply) == 0) {
			dbus_error_free(&err);
			return;
		}

		error("Agent replied with an error: %s, %s",
				err.name, err.message);
		cb(agent, &err, 0, req->user_data);
		dbus_error_free(&err);
		goto done;
	}

	dbus_error_init(&err);
	if (!dbus_message_get_args(message, &err,
				DBUS_TYPE_UINT32, &passkey,
				DBUS_TYPE_INVALID)) {
		error("Wrong passkey reply signature: %s", err.message);
		cb(agent, &err, 0, req->user_data);
		dbus_error_free(&err);
		goto done;
	}

	cb(agent, NULL, passkey, req->user_data);

done:
	if (message)
		dbus_message_unref(message);

	dbus_pending_call_cancel(req->call);
	agent->request = NULL;
	agent_request_free(req);
}

static int passkey_request_new(struct agent_request *req,
				const char *device_path)
{
	struct agent *agent = req->agent;

	req->msg = dbus_message_new_method_call(agent->name, agent->path,
					"org.bluez.Agent", "RequestPasskey");
	if (req->msg == NULL) {
		error("Couldn't allocate D-Bus message");
		return -ENOMEM;
	}

	dbus_message_append_args(req->msg, DBUS_TYPE_OBJECT_PATH, &device_path,
					DBUS_TYPE_INVALID);

	if (dbus_connection_send_with_reply(connection, req->msg,
					&req->call, REQUEST_TIMEOUT) == FALSE) {
		error("D-Bus send failed");
		return -EIO;
	}

	dbus_pending_call_set_notify(req->call, passkey_reply, req, NULL);
	return 0;
}

int agent_request_passkey(struct agent *agent, struct btd_device *device,
				agent_passkey_cb cb, void *user_data)
{
	struct agent_request *req;
	const gchar *dev_path = device_get_path(device);
	int err;

	if (agent->request)
		return -EBUSY;

	debug("Calling Agent.RequestPasskey: name=%s, path=%s",
			agent->name, agent->path);

	req = agent_request_new(agent, AGENT_REQUEST_PASSKEY, cb, user_data);

	err = passkey_request_new(req, dev_path);
	if (err < 0)
		goto failed;

	agent->request = req;

	return 0;

failed:
	agent_request_free(req);
	return err;
}

static int confirmation_request_new(struct agent_request *req,
					const char *device_path,
					uint32_t passkey)
{
	struct agent *agent = req->agent;

	req->msg = dbus_message_new_method_call(agent->name, agent->path,
				"org.bluez.Agent", "RequestConfirmation");
	if (req->msg == NULL) {
		error("Couldn't allocate D-Bus message");
		return -ENOMEM;
	}

	dbus_message_append_args(req->msg,
				DBUS_TYPE_OBJECT_PATH, &device_path,
				DBUS_TYPE_UINT32, &passkey,
				DBUS_TYPE_INVALID);

	if (dbus_connection_send_with_reply(connection, req->msg,
				&req->call, REQUEST_TIMEOUT) == FALSE) {
		error("D-Bus send failed");
		return -EIO;
	}

	dbus_pending_call_set_notify(req->call, simple_agent_reply, req, NULL);

	return 0;
}

int agent_request_confirmation(struct agent *agent, struct btd_device *device,
				uint32_t passkey, agent_cb cb,
				void *user_data)
{
	struct agent_request *req;
	const gchar *dev_path = device_get_path(device);
	int err;

	if (agent->request)
		return -EBUSY;

	debug("Calling Agent.RequestConfirmation: name=%s, path=%s, passkey=%06u",
			agent->name, agent->path, passkey);

	req = agent_request_new(agent, AGENT_REQUEST_CONFIRMATION, cb,
				user_data);

	err = confirmation_request_new(req, dev_path, passkey);
	if (err < 0)
		goto failed;

	agent->request = req;

	return 0;

failed:
	agent_request_free(req);
	return err;
}

static int request_fallback(struct agent_request *req,
				DBusPendingCallNotifyFunction function)
{
	struct btd_adapter *adapter = req->agent->adapter;
	struct agent *adapter_agent = adapter_get_agent(adapter);
	DBusMessage *msg;

	if (req->agent == adapter_agent || adapter_agent == NULL)
		return -EINVAL;

	dbus_pending_call_cancel(req->call);

	msg = dbus_message_copy(req->msg);

	dbus_message_set_destination(msg, adapter_agent->name);
	dbus_message_set_path(msg, adapter_agent->path);

	if (dbus_connection_send_with_reply(connection, msg,
					&req->call, REQUEST_TIMEOUT) == FALSE) {
		error("D-Bus send failed");
		dbus_message_unref(msg);
		return -EIO;
	}

	req->agent->request = NULL;
	req->agent = adapter_agent;
	req->agent->request = req;

	dbus_message_unref(req->msg);
	req->msg = msg;

	dbus_pending_call_set_notify(req->call, function, req, NULL);

	return 0;
}

int agent_display_passkey(struct agent *agent, struct btd_device *device,
				uint32_t passkey)
{
	DBusMessage *message;
	const gchar *dev_path = device_get_path(device);

	message = dbus_message_new_method_call(agent->name, agent->path,
				"org.bluez.Agent", "DisplayPasskey");
	if (!message) {
		error("Couldn't allocate D-Bus message");
		return -1;
	}

	dbus_message_append_args(message,
				DBUS_TYPE_OBJECT_PATH, &dev_path,
				DBUS_TYPE_UINT32, &passkey,
				DBUS_TYPE_INVALID);

	if (!g_dbus_send_message(connection, message)) {
		error("D-Bus send failed");
		dbus_message_unref(message);
		return -1;
	}

	return 0;
}

uint8_t agent_get_io_capability(struct agent *agent)
{
	return agent->capability;
}

gboolean agent_matches(struct agent *agent, const char *name, const char *path)
{
	if (g_str_equal(agent->name, name) && g_str_equal(agent->path, path))
		return TRUE;

	return FALSE;
}

gboolean agent_is_busy(struct agent *agent, void *user_data)
{
	if (!agent->request)
		return FALSE;

	if (user_data && user_data != agent->request->user_data)
		return FALSE;

	return TRUE;
}

void agent_exit(void)
{
	dbus_connection_unref(connection);
}

void agent_init(void)
{
	connection = dbus_bus_get(DBUS_BUS_SYSTEM, NULL);
}
