/*
 *
 *  Connection Manager
 *
 *  Copyright (C) 2012-2014  BMW Car IT GmbH.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  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 <string.h>
#include <errno.h>

#include <gdbus.h>

#define CONNMAN_API_SUBJECT_TO_CHANGE
#include <connman/plugin.h>
#include <connman/device.h>
#include <connman/network.h>
#include <connman/service.h>
#include <connman/inet.h>
#include <connman/dbus.h>

#define DUNDEE_SERVICE			"org.ofono.dundee"
#define DUNDEE_MANAGER_INTERFACE	DUNDEE_SERVICE ".Manager"
#define DUNDEE_DEVICE_INTERFACE		DUNDEE_SERVICE ".Device"

#define DEVICE_ADDED			"DeviceAdded"
#define DEVICE_REMOVED			"DeviceRemoved"
#define PROPERTY_CHANGED		"PropertyChanged"

#define GET_PROPERTIES			"GetProperties"
#define SET_PROPERTY			"SetProperty"
#define GET_DEVICES			"GetDevices"

#define TIMEOUT 60000

static DBusConnection *connection;

static GHashTable *dundee_devices = NULL;

struct dundee_data {
	char *path;
	char *name;

	struct connman_device *device;
	struct connman_network *network;

	bool active;

	int index;

	/* IPv4 Settings */
	enum connman_ipconfig_method method;
	struct connman_ipaddress *address;
	char *nameservers;

	DBusPendingCall *call;
};

static char *get_ident(const char *path)
{
	char *pos;

	if (*path != '/')
		return NULL;

	pos = strrchr(path, '/');
	if (!pos)
		return NULL;

	return pos + 1;
}

static int create_device(struct dundee_data *info)
{
	struct connman_device *device;
	char *ident;
	int err;

	DBG("%s", info->path);

	ident = g_strdup(get_ident(info->path));
	device = connman_device_create("dundee", CONNMAN_DEVICE_TYPE_BLUETOOTH);
	if (!device) {
		err = -ENOMEM;
		goto out;
	}

	DBG("device %p", device);

	connman_device_set_ident(device, ident);

	connman_device_set_string(device, "Path", info->path);

	connman_device_set_data(device, info);

	err = connman_device_register(device);
	if (err < 0) {
		connman_error("Failed to register DUN device");
		connman_device_unref(device);
		goto out;
	}

	info->device = device;

out:
	g_free(ident);
	return err;
}

static void destroy_device(struct dundee_data *info)
{
	connman_device_set_powered(info->device, false);

	if (info->call)
		dbus_pending_call_cancel(info->call);

	if (info->network) {
		connman_device_remove_network(info->device, info->network);
		connman_network_unref(info->network);
		info->network = NULL;
	}

	connman_device_unregister(info->device);
	connman_device_unref(info->device);

	info->device = NULL;
}

static void device_destroy(gpointer data)
{
	struct dundee_data *info = data;

	if (info->device)
		destroy_device(info);

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

	g_free(info);
}

static int create_network(struct dundee_data *info)
{
	struct connman_network *network;
	const char *group;
	int err;

	DBG("%s", info->path);

	network = connman_network_create(info->path,
				CONNMAN_NETWORK_TYPE_BLUETOOTH_DUN);
	if (!network)
		return -ENOMEM;

	DBG("network %p", network);

	connman_network_set_data(network, info);

	connman_network_set_string(network, "Path",
				info->path);

	connman_network_set_name(network, info->name);

	group = get_ident(info->path);
	connman_network_set_group(network, group);

	err = connman_device_add_network(info->device, network);
	if (err < 0) {
		connman_network_unref(network);
		return err;
	}

	info->network = network;

	return 0;
}

static void set_connected(struct dundee_data *info)
{
	struct connman_service *service;

	DBG("%s", info->path);

	connman_inet_ifup(info->index);

	service = connman_service_lookup_from_network(info->network);
	if (!service)
		return;

	connman_service_create_ip4config(service, info->index);
	connman_network_set_index(info->network, info->index);
	connman_network_set_ipv4_method(info->network,
					CONNMAN_IPCONFIG_METHOD_FIXED);
	connman_network_set_ipaddress(info->network, info->address);
	connman_network_set_nameservers(info->network, info->nameservers);

	connman_network_set_connected(info->network, true);
}

static void set_disconnected(struct dundee_data *info)
{
	DBG("%s", info->path);

	connman_network_set_connected(info->network, false);
	connman_inet_ifdown(info->index);
}

static void set_property_reply(DBusPendingCall *call, void *user_data)
{
	struct dundee_data *info = user_data;
	DBusMessage *reply;
	DBusError error;

	DBG("%s", info->path);

	info->call = NULL;

	dbus_error_init(&error);

	reply = dbus_pending_call_steal_reply(call);

	if (dbus_set_error_from_message(&error, reply)) {
		connman_error("Failed to change property: %s %s %s",
				info->path, error.name, error.message);
		dbus_error_free(&error);

		connman_network_set_error(info->network,
					CONNMAN_NETWORK_ERROR_ASSOCIATE_FAIL);
	}

	dbus_message_unref(reply);

	dbus_pending_call_unref(call);
}

static int set_property(struct dundee_data *info,
			const char *property, int type, void *value)
{
	DBusMessage *message;
	DBusMessageIter iter;

	DBG("%s %s", info->path, property);

	message = dbus_message_new_method_call(DUNDEE_SERVICE, info->path,
					DUNDEE_DEVICE_INTERFACE, SET_PROPERTY);
	if (!message)
		return -ENOMEM;

	dbus_message_iter_init_append(message, &iter);
	connman_dbus_property_append_basic(&iter, property, type, value);

	if (!dbus_connection_send_with_reply(connection, message,
						&info->call, TIMEOUT)) {
		connman_error("Failed to change property: %s %s",
				info->path, property);
		dbus_message_unref(message);
		return -EINVAL;
	}

	if (!info->call) {
		connman_error("D-Bus connection not available");
		dbus_message_unref(message);
		return -EINVAL;
	}

	dbus_pending_call_set_notify(info->call, set_property_reply,
					info, NULL);

	dbus_message_unref(message);

	return -EINPROGRESS;
}

static int device_set_active(struct dundee_data *info)
{
	dbus_bool_t active = TRUE;

	DBG("%s", info->path);

	return set_property(info, "Active", DBUS_TYPE_BOOLEAN,
				&active);
}

static int device_set_inactive(struct dundee_data *info)
{
	dbus_bool_t active = FALSE;
	int err;

	DBG("%s", info->path);

	err = set_property(info, "Active", DBUS_TYPE_BOOLEAN,
				&active);
	if (err == -EINPROGRESS)
		return 0;

	return err;
}

static int network_probe(struct connman_network *network)
{
	DBG("network %p", network);

	return 0;
}

static void network_remove(struct connman_network *network)
{
	DBG("network %p", network);
}

static int network_connect(struct connman_network *network)
{
	struct dundee_data *info = connman_network_get_data(network);

	DBG("network %p", network);

	return device_set_active(info);
}

static int network_disconnect(struct connman_network *network)
{
	struct dundee_data *info = connman_network_get_data(network);

	DBG("network %p", network);

	return device_set_inactive(info);
}

static struct connman_network_driver network_driver = {
	.name		= "network",
	.type		= CONNMAN_NETWORK_TYPE_BLUETOOTH_DUN,
	.probe		= network_probe,
	.remove		= network_remove,
	.connect	= network_connect,
	.disconnect	= network_disconnect,
};

static int dundee_probe(struct connman_device *device)
{
	GHashTableIter iter;
	gpointer key, value;

	DBG("device %p", device);

	if (!dundee_devices)
		return -ENOTSUP;

	g_hash_table_iter_init(&iter, dundee_devices);

	while (g_hash_table_iter_next(&iter, &key, &value)) {
		struct dundee_data *info = value;

		if (device == info->device)
			return 0;
	}

	return -ENOTSUP;
}

static void dundee_remove(struct connman_device *device)
{
	DBG("device %p", device);
}

static int dundee_enable(struct connman_device *device)
{
	DBG("device %p", device);

	return 0;
}

static int dundee_disable(struct connman_device *device)
{
	DBG("device %p", device);

	return 0;
}

static struct connman_device_driver dundee_driver = {
	.name		= "dundee",
	.type		= CONNMAN_DEVICE_TYPE_BLUETOOTH,
	.probe		= dundee_probe,
	.remove		= dundee_remove,
	.enable		= dundee_enable,
	.disable	= dundee_disable,
};

static char *extract_nameservers(DBusMessageIter *array)
{
	DBusMessageIter entry;
	char *nameservers = NULL;
	char *tmp;

	dbus_message_iter_recurse(array, &entry);

	while (dbus_message_iter_get_arg_type(&entry) == DBUS_TYPE_STRING) {
		const char *nameserver;

		dbus_message_iter_get_basic(&entry, &nameserver);

		if (!nameservers) {
			nameservers = g_strdup(nameserver);
		} else {
			tmp = nameservers;
			nameservers = g_strdup_printf("%s %s", tmp, nameserver);
			g_free(tmp);
		}

		dbus_message_iter_next(&entry);
	}

	return nameservers;
}

static void extract_settings(DBusMessageIter *array,
				struct dundee_data *info)
{
	DBusMessageIter dict;
	char *address = NULL, *gateway = NULL;
	char *nameservers = NULL;
	const char *interface = NULL;
	int index = -1;

	if (dbus_message_iter_get_arg_type(array) != DBUS_TYPE_ARRAY)
		return;

	dbus_message_iter_recurse(array, &dict);

	while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {
		DBusMessageIter entry, value;
		const char *key, *val;

		dbus_message_iter_recurse(&dict, &entry);
		dbus_message_iter_get_basic(&entry, &key);

		dbus_message_iter_next(&entry);
		dbus_message_iter_recurse(&entry, &value);

		if (g_str_equal(key, "Interface")) {
			dbus_message_iter_get_basic(&value, &interface);

			DBG("Interface %s", interface);

			index = connman_inet_ifindex(interface);

			DBG("index %d", index);

			if (index < 0)
				break;
		} else if (g_str_equal(key, "Address")) {
			dbus_message_iter_get_basic(&value, &val);

			address = g_strdup(val);

			DBG("Address %s", address);
		} else if (g_str_equal(key, "DomainNameServers")) {
			nameservers = extract_nameservers(&value);

			DBG("Nameservers %s", nameservers);
		} else if (g_str_equal(key, "Gateway")) {
			dbus_message_iter_get_basic(&value, &val);

			gateway = g_strdup(val);

			DBG("Gateway %s", gateway);
		}

		dbus_message_iter_next(&dict);
	}

	if (index < 0)
		goto out;

	info->address = connman_ipaddress_alloc(AF_INET);
	if (!info->address)
		goto out;

	info->index = index;
	connman_ipaddress_set_ipv4(info->address, address, NULL, gateway);

	info->nameservers = nameservers;

out:
	if (info->nameservers != nameservers)
		g_free(nameservers);

	g_free(address);
	g_free(gateway);
}

static gboolean device_changed(DBusConnection *conn,
				DBusMessage *message,
				void *user_data)
{
	const char *path = dbus_message_get_path(message);
	struct dundee_data *info = NULL;
	DBusMessageIter iter, value;
	const char *key;
	const char *signature =	DBUS_TYPE_STRING_AS_STRING
		DBUS_TYPE_VARIANT_AS_STRING;

	if (!dbus_message_has_signature(message, signature)) {
		connman_error("dundee signature does not match");
		return TRUE;
	}

	info = g_hash_table_lookup(dundee_devices, path);
	if (!info)
		return TRUE;

	if (!dbus_message_iter_init(message, &iter))
		return TRUE;

	dbus_message_iter_get_basic(&iter, &key);

	dbus_message_iter_next(&iter);
	dbus_message_iter_recurse(&iter, &value);

	/*
	 * Dundee guarantees the ordering of Settings and
	 * Active. Settings will always be send before Active = True.
	 * That means we don't have to order here.
	 */
	if (g_str_equal(key, "Active")) {
		dbus_bool_t active;

		dbus_message_iter_get_basic(&value, &active);
		info->active = active;

		DBG("%s Active %d", info->path, info->active);

		if (info->active)
			set_connected(info);
		else
			set_disconnected(info);
	} else if (g_str_equal(key, "Settings")) {
		DBG("%s Settings", info->path);

		extract_settings(&value, info);
	} else if (g_str_equal(key, "Name")) {
		char *name;

		dbus_message_iter_get_basic(&value, &name);

		g_free(info->name);
		info->name = g_strdup(name);

		DBG("%s Name %s", info->path, info->name);

		connman_network_set_name(info->network, info->name);
		connman_network_update(info->network);
	}

	return TRUE;
}

static void add_device(const char *path, DBusMessageIter *properties)
{
	struct dundee_data *info;
	int err;

	info = g_hash_table_lookup(dundee_devices, path);
	if (info)
		return;

	info = g_try_new0(struct dundee_data, 1);
	if (!info)
		return;

	info->path = g_strdup(path);

	while (dbus_message_iter_get_arg_type(properties) ==
			DBUS_TYPE_DICT_ENTRY) {
		DBusMessageIter entry, value;
		const char *key;

		dbus_message_iter_recurse(properties, &entry);
		dbus_message_iter_get_basic(&entry, &key);

		dbus_message_iter_next(&entry);
		dbus_message_iter_recurse(&entry, &value);

		if (g_str_equal(key, "Active")) {
			dbus_bool_t active;

			dbus_message_iter_get_basic(&value, &active);
			info->active = active;

			DBG("%s Active %d", info->path, info->active);
		} else if (g_str_equal(key, "Settings")) {
			DBG("%s Settings", info->path);

			extract_settings(&value, info);
		} else if (g_str_equal(key, "Name")) {
			char *name;

			dbus_message_iter_get_basic(&value, &name);

			info->name = g_strdup(name);

			DBG("%s Name %s", info->path, info->name);
		}

		dbus_message_iter_next(properties);
	}

	g_hash_table_insert(dundee_devices, g_strdup(path), info);

	err = create_device(info);
	if (err < 0)
		goto out;

	err = create_network(info);
	if (err < 0) {
		destroy_device(info);
		goto out;
	}

	if (info->active)
		set_connected(info);

	return;

out:
	g_hash_table_remove(dundee_devices, path);
}

static gboolean device_added(DBusConnection *conn, DBusMessage *message,
				void *user_data)
{
	DBusMessageIter iter, properties;
	const char *path;
	const char *signature = DBUS_TYPE_OBJECT_PATH_AS_STRING
		DBUS_TYPE_ARRAY_AS_STRING
		DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
		DBUS_TYPE_STRING_AS_STRING
		DBUS_TYPE_VARIANT_AS_STRING
		DBUS_DICT_ENTRY_END_CHAR_AS_STRING;

	if (!dbus_message_has_signature(message, signature)) {
		connman_error("dundee signature does not match");
		return TRUE;
	}

	DBG("");

	if (!dbus_message_iter_init(message, &iter))
		return TRUE;

	dbus_message_iter_get_basic(&iter, &path);

	dbus_message_iter_next(&iter);
	dbus_message_iter_recurse(&iter, &properties);

	add_device(path, &properties);

	return TRUE;
}

static void remove_device(DBusConnection *conn, const char *path)
{
	DBG("path %s", path);

	g_hash_table_remove(dundee_devices, path);
}

static gboolean device_removed(DBusConnection *conn, DBusMessage *message,
				void *user_data)
{
	const char *path;
	const char *signature = DBUS_TYPE_OBJECT_PATH_AS_STRING;

	if (!dbus_message_has_signature(message, signature)) {
		connman_error("dundee signature does not match");
		return TRUE;
	}

	dbus_message_get_args(message, NULL, DBUS_TYPE_OBJECT_PATH, &path,
				DBUS_TYPE_INVALID);
	remove_device(conn, path);
	return TRUE;
}

static void manager_get_devices_reply(DBusPendingCall *call, void *user_data)
{
	DBusMessage *reply;
	DBusError error;
	DBusMessageIter array, dict;
	const char *signature = DBUS_TYPE_ARRAY_AS_STRING
		DBUS_STRUCT_BEGIN_CHAR_AS_STRING
		DBUS_TYPE_OBJECT_PATH_AS_STRING
		DBUS_TYPE_ARRAY_AS_STRING
		DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
		DBUS_TYPE_STRING_AS_STRING
		DBUS_TYPE_VARIANT_AS_STRING
		DBUS_DICT_ENTRY_END_CHAR_AS_STRING
		DBUS_STRUCT_END_CHAR_AS_STRING;

	DBG("");

	reply = dbus_pending_call_steal_reply(call);

	if (!dbus_message_has_signature(reply, signature)) {
		connman_error("dundee signature does not match");
		goto done;
	}

	dbus_error_init(&error);

	if (dbus_set_error_from_message(&error, reply)) {
		connman_error("%s", error.message);
		dbus_error_free(&error);
		goto done;
	}

	if (!dbus_message_iter_init(reply, &array))
		goto done;

	dbus_message_iter_recurse(&array, &dict);

	while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_STRUCT) {
		DBusMessageIter value, properties;
		const char *path;

		dbus_message_iter_recurse(&dict, &value);
		dbus_message_iter_get_basic(&value, &path);

		dbus_message_iter_next(&value);
		dbus_message_iter_recurse(&value, &properties);

		add_device(path, &properties);

		dbus_message_iter_next(&dict);
	}

done:
	dbus_message_unref(reply);

	dbus_pending_call_unref(call);
}

static int manager_get_devices(void)
{
	DBusMessage *message;
	DBusPendingCall *call;

	DBG("");

	message = dbus_message_new_method_call(DUNDEE_SERVICE, "/",
					DUNDEE_MANAGER_INTERFACE, GET_DEVICES);
	if (!message)
		return -ENOMEM;

	if (!dbus_connection_send_with_reply(connection, message,
						&call, TIMEOUT)) {
		connman_error("Failed to call GetDevices()");
		dbus_message_unref(message);
		return -EINVAL;
	}

	if (!call) {
		connman_error("D-Bus connection not available");
		dbus_message_unref(message);
		return -EINVAL;
	}

	dbus_pending_call_set_notify(call, manager_get_devices_reply,
					NULL, NULL);

	dbus_message_unref(message);

	return -EINPROGRESS;
}

static void dundee_connect(DBusConnection *conn, void *user_data)
{
	DBG("connection %p", conn);

	dundee_devices = g_hash_table_new_full(g_str_hash, g_str_equal,
					g_free, device_destroy);

	manager_get_devices();
}

static void dundee_disconnect(DBusConnection *conn, void *user_data)
{
	DBG("connection %p", conn);

	g_hash_table_destroy(dundee_devices);
	dundee_devices = NULL;
}

static guint watch;
static guint added_watch;
static guint removed_watch;
static guint device_watch;

static int dundee_init(void)
{
	int err;

	connection = connman_dbus_get_connection();
	if (!connection)
		return -EIO;

	watch = g_dbus_add_service_watch(connection, DUNDEE_SERVICE,
			dundee_connect, dundee_disconnect, NULL, NULL);

	added_watch = g_dbus_add_signal_watch(connection, DUNDEE_SERVICE, NULL,
						DUNDEE_MANAGER_INTERFACE,
						DEVICE_ADDED, device_added,
						NULL, NULL);

	removed_watch = g_dbus_add_signal_watch(connection, DUNDEE_SERVICE,
						NULL, DUNDEE_MANAGER_INTERFACE,
						DEVICE_REMOVED, device_removed,
						NULL, NULL);

	device_watch = g_dbus_add_signal_watch(connection, DUNDEE_SERVICE,
						NULL, DUNDEE_DEVICE_INTERFACE,
						PROPERTY_CHANGED,
						device_changed,
						NULL, NULL);


	if (watch == 0 || added_watch == 0 || removed_watch == 0 ||
			device_watch == 0) {
		err = -EIO;
		goto remove;
	}

	err = connman_network_driver_register(&network_driver);
	if (err < 0)
		goto remove;

	err = connman_device_driver_register(&dundee_driver);
	if (err < 0) {
		connman_network_driver_unregister(&network_driver);
		goto remove;
	}

	return 0;

remove:
	g_dbus_remove_watch(connection, watch);
	g_dbus_remove_watch(connection, added_watch);
	g_dbus_remove_watch(connection, removed_watch);
	g_dbus_remove_watch(connection, device_watch);

	dbus_connection_unref(connection);

	return err;
}

static void dundee_exit(void)
{
	g_dbus_remove_watch(connection, watch);
	g_dbus_remove_watch(connection, added_watch);
	g_dbus_remove_watch(connection, removed_watch);
	g_dbus_remove_watch(connection, device_watch);

	connman_device_driver_unregister(&dundee_driver);
	connman_network_driver_unregister(&network_driver);

	dbus_connection_unref(connection);
}

CONNMAN_PLUGIN_DEFINE(dundee, "Dundee plugin", VERSION,
		CONNMAN_PLUGIN_PRIORITY_DEFAULT, dundee_init, dundee_exit)
