/*
 *
 *  neard - Near Field Communication manager
 *
 *  Copyright (C) 2011  Intel Corporation. All rights reserved.
 *
 *  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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

#include <glib.h>

#include <gdbus.h>

#include "near.h"

struct near_device {
	char *path;

	uint32_t adapter_idx;
	uint32_t target_idx;

	uint8_t nfcid[NFC_MAX_NFCID1_LEN];
	uint8_t nfcid_len;

	size_t data_length;
	uint8_t *data;

	uint32_t n_records;
	GList *records;

	DBusMessage *push_msg; /* Push pending message */
};

static DBusConnection *connection = NULL;

static GHashTable *device_hash;

static GSList *driver_list = NULL;

static void free_device(gpointer data)
{
	struct near_device *device = data;

	DBG("device %p", device);

	near_ndef_records_free(device->records);

	g_dbus_unregister_interface(connection, device->path,
						NFC_DEVICE_INTERFACE);

	g_free(device->path);
	g_free(device->data);
	g_free(device);
}

struct near_device *near_device_get_device(uint32_t adapter_idx,
						uint32_t target_idx)
{
	struct near_device *device;
	char *path;

	DBG("");

	path = g_strdup_printf("%s/nfc%u/device%u", NFC_PATH,
					adapter_idx, target_idx);
	if (!path)
		return NULL;

	device = g_hash_table_lookup(device_hash, path);
	g_free(path);

	/* TODO refcount */
	return device;
}

const char *__near_device_get_path(struct near_device *device)
{
	return device->path;
}

uint32_t __neard_device_get_idx(struct near_device *device)
{
	return device->target_idx;
}

static void push_cb(uint32_t adapter_idx, uint32_t target_idx, int status)
{
	struct near_device *device;
	DBusConnection *conn;
	DBusMessage *reply;

	DBG("Push status %d", status);

	conn = near_dbus_get_connection();
	device = near_device_get_device(adapter_idx, target_idx);

	if (!conn || !device)
		return;

	if (status != 0) {
		reply = __near_error_failed(device->push_msg, -status);
		if (reply)
			g_dbus_send_message(conn, reply);
	} else {
		g_dbus_send_reply(conn, device->push_msg, DBUS_TYPE_INVALID);
	}

	dbus_message_unref(device->push_msg);
	device->push_msg = NULL;
}

static char *sn_from_message(DBusMessage *msg)
{
	DBusMessageIter iter;
	DBusMessageIter arr_iter;

	DBG("");

	dbus_message_iter_init(msg, &iter);
	dbus_message_iter_recurse(&iter, &arr_iter);

	while (dbus_message_iter_get_arg_type(&arr_iter) !=
						DBUS_TYPE_INVALID) {
		const char *key, *value;
		DBusMessageIter ent_iter;
		DBusMessageIter var_iter;

		dbus_message_iter_recurse(&arr_iter, &ent_iter);
		dbus_message_iter_get_basic(&ent_iter, &key);

		if (g_strcmp0(key, "Type") != 0) {
			dbus_message_iter_next(&arr_iter);
			continue;
		}

		dbus_message_iter_next(&ent_iter);
		dbus_message_iter_recurse(&ent_iter, &var_iter);

		switch (dbus_message_iter_get_arg_type(&var_iter)) {
		case DBUS_TYPE_STRING:
			dbus_message_iter_get_basic(&var_iter, &value);

			if (g_strcmp0(value, "Text") == 0)
				return NEAR_DEVICE_SN_SNEP;
			else if (g_strcmp0(value, "URI") == 0)
				return NEAR_DEVICE_SN_SNEP;
			else if (g_strcmp0(value, "SmartPoster") == 0)
				return NEAR_DEVICE_SN_SNEP;
			else if (g_strcmp0(value, "Handover") == 0)
				return NEAR_DEVICE_SN_HANDOVER;
			else if (g_strcmp0(value, "StaticHandover") == 0)
				return NEAR_DEVICE_SN_HANDOVER;
			else if (g_strcmp0(value, "MIME") == 0)
				return NEAR_DEVICE_SN_SNEP;
			else
				return NULL;

			break;
		}

		dbus_message_iter_next(&arr_iter);
	}

	return NULL;
}

static DBusMessage *push_ndef(DBusConnection *conn,
				DBusMessage *msg, void *data)
{
	struct near_device *device = data;
	struct near_ndef_message *ndef;
	char *service_name;
	int err;

	DBG("conn %p", conn);

	if (device->push_msg)
		return __near_error_in_progress(msg);

	device->push_msg = dbus_message_ref(msg);

	service_name = sn_from_message(msg);
	if (!service_name) {
		err = -EINVAL;
		goto error;
	}

	ndef = __ndef_build_from_message(msg);
	if (!ndef) {
		err = -EINVAL;
		goto error;
	}

	err = __near_device_push(device, ndef, service_name, push_cb);
	if (err < 0)
		goto error;

	return NULL;

error:
	dbus_message_unref(device->push_msg);
	device->push_msg = NULL;

	return __near_error_failed(msg, -err);
}

static gboolean property_get_adapter(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *user_data)
{
	struct near_device *device = user_data;
	struct near_adapter *adapter;
	const char *path;

	adapter = __near_adapter_get(device->adapter_idx);
	if (!adapter)
		return FALSE;

	path = __near_adapter_get_path(adapter);
	if (!path)
		return FALSE;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH, &path);

	return TRUE;

}

static const GDBusMethodTable device_methods[] = {
	{ GDBUS_ASYNC_METHOD("Push", GDBUS_ARGS({"attributes", "a{sv}"}),
							NULL, push_ndef) },
	{ },
};

static const GDBusPropertyTable device_properties[] = {
	{ "Adapter", "o", property_get_adapter },

	{ }
};

void __near_device_remove(struct near_device *device)
{
	char *path = device->path;

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

	if (device->push_msg)
		push_cb(device->adapter_idx, device->target_idx, EIO);

	g_hash_table_remove(device_hash, path);
}

int near_device_add_data(uint32_t adapter_idx, uint32_t target_idx,
			uint8_t *data, size_t data_length)
{
	struct near_device *device;

	device = near_device_get_device(adapter_idx, target_idx);
	if (!device)
		return -ENODEV;

	device->data_length = data_length;
	device->data = g_try_malloc0(data_length);
	if (!device->data)
		return -ENOMEM;

	if (data)
		memcpy(device->data, data, data_length);

	return 0;
}

int near_device_add_records(struct near_device *device, GList *records,
				near_device_io_cb cb, int status)
{
	GList *list;
	struct near_ndef_record *record;
	char *path;

	DBG("records %p", records);

	near_ndef_records_free(device->records);
	device->records = NULL;

	for (list = records; list; list = list->next) {
		record = list->data;

		path = g_strdup_printf("%s/nfc%u/device%u/record%u",
					NFC_PATH, device->adapter_idx,
					device->target_idx, device->n_records);

		if (!path)
			continue;

		__near_ndef_record_register(record, path);

		device->n_records++;
		device->records = g_list_append(device->records, record);
	}

	__near_agent_ndef_parse_records(device->records);

	if (cb)
		cb(device->adapter_idx, device->target_idx, status);

	g_list_free(records);

	return 0;
}

struct near_device *__near_device_add(uint32_t adapter_idx, uint32_t target_idx,
					uint8_t *nfcid, uint8_t nfcid_len)
{
	struct near_device *device;
	char *path;

	device = near_device_get_device(adapter_idx, target_idx);
	if (device)
		return NULL;

	device = g_try_malloc0(sizeof(struct near_device));
	if (!device)
		return NULL;

	device->path = g_strdup_printf("%s/nfc%u/device%u", NFC_PATH,
					adapter_idx, target_idx);
	if (!device->path) {
		g_free(device);
		return NULL;
	}
	device->adapter_idx = adapter_idx;
	device->target_idx = target_idx;
	device->n_records = 0;

	if (nfcid_len <= NFC_MAX_NFCID1_LEN && nfcid_len > 0) {
		device->nfcid_len = nfcid_len;
		memcpy(device->nfcid, nfcid, nfcid_len);
	}

	path = g_strdup(device->path);
	if (!path) {
		g_free(device);
		return NULL;
	}

	g_hash_table_insert(device_hash, path, device);

	return device;
}

bool __near_device_register_interface(struct near_device *device)
{
	DBG("connection %p", connection);

	return g_dbus_register_interface(connection, device->path,
					NFC_DEVICE_INTERFACE,
					device_methods, NULL,
					device_properties, device, NULL);
}

int __near_device_listen(struct near_device *device, near_device_io_cb cb)
{
	GSList *list;

	DBG("");

	for (list = driver_list; list; list = list->next) {
		struct near_device_driver *driver = list->data;

		return driver->listen(device->adapter_idx, cb);
	}

	return 0;
}

int __near_device_push(struct near_device *device,
			struct near_ndef_message *ndef, char *service_name,
			near_device_io_cb cb)
{
	GSList *list;

	DBG("");

	if (!__near_adapter_get_dep_state(device->adapter_idx)) {
		near_error("DEP link is not established");
		return -ENOLINK;
	}

	for (list = driver_list; list; list = list->next) {
		struct near_device_driver *driver = list->data;

		return driver->push(device->adapter_idx, device->target_idx,
					ndef, service_name, cb);
	}

	return 0;
}

static gint cmp_prio(gconstpointer a, gconstpointer b)
{
	const struct near_tag_driver *driver1 = a;
	const struct near_tag_driver *driver2 = b;

	return driver2->priority - driver1->priority;
}

int near_device_driver_register(struct near_device_driver *driver)
{
	DBG("");

	if (!driver->listen)
		return -EINVAL;

	driver_list = g_slist_insert_sorted(driver_list, driver, cmp_prio);

	__near_adapter_listen(driver);

	return 0;
}

void near_device_driver_unregister(struct near_device_driver *driver)
{
	DBG("");

	driver_list = g_slist_remove(driver_list, driver);
}

int __near_device_init(void)
{
	DBG("");

	connection = near_dbus_get_connection();

	device_hash = g_hash_table_new_full(g_str_hash, g_str_equal,
						g_free, free_device);

	return 0;
}

void __near_device_cleanup(void)
{
	DBG("");

	g_hash_table_destroy(device_hash);
	device_hash = NULL;
}
