/*
 *
 *  BlueZ - Bluetooth protocol stack for Linux
 *
 *  Copyright (C) 2006-2010  Nokia Corporation
 *  Copyright (C) 2004-2010  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 <glib.h>
#include <dbus/dbus.h>

#include "adapter.h"
#include "plugin.h"
#include "log.h"
#include "gdbus.h"

/* from mce/mode-names.h */
#define MCE_RADIO_STATE_BLUETOOTH	(1 << 3)

/* from mce/dbus-names.h */
#define MCE_SERVICE			"com.nokia.mce"
#define MCE_REQUEST_IF			"com.nokia.mce.request"
#define MCE_SIGNAL_IF			"com.nokia.mce.signal"
#define MCE_REQUEST_PATH		"/com/nokia/mce/request"
#define MCE_SIGNAL_PATH			"/com/nokia/mce/signal"
#define MCE_RADIO_STATES_CHANGE_REQ	"req_radio_states_change"
#define MCE_RADIO_STATES_GET		"get_radio_states"
#define MCE_RADIO_STATES_SIG		"radio_states_ind"

static guint watch_id;
static DBusConnection *conn = NULL;
static gboolean mce_bt_set = FALSE;
static gboolean collision = FALSE;

static gboolean mce_signal_callback(DBusConnection *connection,
					DBusMessage *message, void *user_data)
{
	DBusMessageIter args;
	uint32_t sigvalue;
	struct btd_adapter *adapter = user_data;

	DBG("received mce signal");

	if (!dbus_message_iter_init(message, &args))
		error("message has no arguments");
	else if (DBUS_TYPE_UINT32 != dbus_message_iter_get_arg_type(&args))
		error("argument is not uint32");
	else {
		dbus_message_iter_get_basic(&args, &sigvalue);
		DBG("got signal with value %u", sigvalue);

		/* set the adapter according to the mce signal
		   and remember the value */
		mce_bt_set = sigvalue & MCE_RADIO_STATE_BLUETOOTH ?
								TRUE : FALSE;

		if (mce_bt_set)
			btd_adapter_switch_online(adapter);
		else
			btd_adapter_switch_offline(adapter);
	}

	return TRUE;
}

static void read_radio_states_cb(DBusPendingCall *call, void *user_data)
{
	DBusError err;
	DBusMessage *reply;
	dbus_uint32_t radio_states;
	struct btd_adapter *adapter = user_data;

	reply = dbus_pending_call_steal_reply(call);

	dbus_error_init(&err);
	if (dbus_set_error_from_message(&err, reply)) {
		error("mce replied with an error: %s, %s",
				err.name, err.message);
		dbus_error_free(&err);
		goto done;
	}

	dbus_error_init(&err);
	if (dbus_message_get_args(reply, &err,
				DBUS_TYPE_UINT32, &radio_states,
				DBUS_TYPE_INVALID) == FALSE) {
		error("unable to parse get_radio_states reply: %s, %s",
							err.name, err.message);
		dbus_error_free(&err);
		goto done;
	}

	DBG("radio_states: %d", radio_states);

	mce_bt_set = radio_states & MCE_RADIO_STATE_BLUETOOTH ? TRUE : FALSE;

	/* check if the adapter has not completed the initial power
	 * cycle, if so delay action to mce_notify_powered */
	collision = mce_bt_set && adapter_powering_down(adapter);

	if (collision)
		goto done;

	if (mce_bt_set)
		btd_adapter_switch_online(adapter);
	else
		btd_adapter_switch_offline(adapter);

done:
	dbus_message_unref(reply);
}

static void adapter_powered(struct btd_adapter *adapter, gboolean powered)
{
	DBusMessage *msg;
	dbus_uint32_t radio_states = 0;
	dbus_uint32_t radio_mask = MCE_RADIO_STATE_BLUETOOTH;
	static gboolean startup = TRUE;

	DBG("adapter_powered called with %d", powered);

	if (startup) {
		startup = FALSE;
		return;
	}

	/* check if the plugin got the get_radio_states reply from the
	 * mce when the adapter was not yet down during the power
	 * cycling when bluetoothd is started */
	if (collision) {
		error("maemo6: powered state collision");
		collision = FALSE;

		if (mce_bt_set)
			btd_adapter_switch_online(adapter);

		return;
	}

	/* nothing to do if the states match */
	if (mce_bt_set == powered)
		return;

	/* set the mce value according to the state of the adapter */
	msg = dbus_message_new_method_call(MCE_SERVICE, MCE_REQUEST_PATH,
				MCE_REQUEST_IF, MCE_RADIO_STATES_CHANGE_REQ);

	if (powered)
		radio_states = MCE_RADIO_STATE_BLUETOOTH;

	dbus_message_append_args(msg, DBUS_TYPE_UINT32, &radio_states,
				DBUS_TYPE_UINT32, &radio_mask,
				DBUS_TYPE_INVALID);

	if (dbus_connection_send(conn, msg, NULL))
		mce_bt_set = powered;
	else
		error("calling %s failed", MCE_RADIO_STATES_CHANGE_REQ);

	dbus_message_unref(msg);
}

static int mce_probe(struct btd_adapter *adapter)
{
	DBusMessage *msg;
	DBusPendingCall *call;

	DBG("path %s", adapter_get_path(adapter));

	msg = dbus_message_new_method_call(MCE_SERVICE, MCE_REQUEST_PATH,
					MCE_REQUEST_IF, MCE_RADIO_STATES_GET);

	if (!dbus_connection_send_with_reply(conn, msg, &call, -1)) {
		error("calling %s failed", MCE_RADIO_STATES_GET);
		dbus_message_unref(msg);
		return -1;
	}

	dbus_pending_call_set_notify(call, read_radio_states_cb, adapter, NULL);
	dbus_pending_call_unref(call);
	dbus_message_unref(msg);

	watch_id = g_dbus_add_signal_watch(conn, NULL, MCE_SIGNAL_PATH,
					MCE_SIGNAL_IF, MCE_RADIO_STATES_SIG,
					mce_signal_callback, adapter, NULL);

	btd_adapter_register_powered_callback(adapter, adapter_powered);

	return 0;
}

static void mce_remove(struct btd_adapter *adapter)
{
	DBG("path %s", adapter_get_path(adapter));

	if (watch_id > 0)
		g_dbus_remove_watch(conn, watch_id);

	btd_adapter_unregister_powered_callback(adapter, adapter_powered);
}

static struct btd_adapter_driver mce_driver = {
	.name	= "mce",
	.probe	= mce_probe,
	.remove	= mce_remove,
};

static int maemo6_init(void)
{
	DBG("init maemo6 plugin");

	conn = dbus_bus_get(DBUS_BUS_SYSTEM, NULL);
	if (conn == NULL) {
		error("Unable to connect to D-Bus");
		return -1;
	}

	return btd_register_adapter_driver(&mce_driver);
}

static void maemo6_exit(void)
{
	DBG("exit maemo6 plugin");

	if (conn != NULL)
		dbus_connection_unref(conn);

	btd_unregister_adapter_driver(&mce_driver);
}

BLUETOOTH_PLUGIN_DEFINE(maemo6, VERSION,
		BLUETOOTH_PLUGIN_PRIORITY_DEFAULT, maemo6_init, maemo6_exit)