/*
 *
 *  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 <unistd.h>
#include <stdint.h>
#include <stdbool.h>
#include <errno.h>
#include <string.h>
#include <poll.h>
#include <sys/socket.h>

#include <linux/socket.h>

#include <near/nfc_copy.h>
#include <near/plugin.h>
#include <near/log.h>
#include <near/types.h>
#include <near/tag.h>
#include <near/device.h>
#include <near/adapter.h>
#include <near/tlv.h>
#include <near/ndef.h>

#include "p2p.h"

static GSList *driver_list = NULL;
static GList *server_list = NULL;

struct p2p_data {
	struct near_p2p_driver *driver;
	uint32_t adapter_idx;
	uint32_t target_idx;
	near_device_io_cb cb;
	int fd;
	guint watch;
	struct p2p_data *server;

	GList *client_list;
};

/* common function for socket binding */
static int __p2p_bind(struct p2p_data *server_data, GIOFunc listener)
{
	int err, fd = server_data->fd;
	struct sockaddr_nfc_llcp addr;
	GIOChannel *channel;
	struct near_p2p_driver *driver = server_data->driver;

	DBG("Binding %s", driver->name);

	memset(&addr, 0, sizeof(struct sockaddr_nfc_llcp));
	addr.sa_family = AF_NFC;
	addr.dev_idx = server_data->adapter_idx;
	addr.nfc_protocol = NFC_PROTO_NFC_DEP;
	addr.service_name_len = strlen(driver->service_name);
	strcpy(addr.service_name, driver->service_name);

	err = bind(fd, (struct sockaddr *) &addr,
			sizeof(struct sockaddr_nfc_llcp));
	if (err < 0) {
		if (errno == EADDRINUSE) {
			DBG("%s is already bound", driver->name);
			err = 0;
			goto out_err;
		}

		near_error("%s bind failed %d %d", driver->name, err, errno);
		goto out_err;
	}

	if (server_data->driver->sock_type == SOCK_STREAM) {
		err = listen(fd, 10);
		if (err < 0) {
			near_error("%s listen failed %d", driver->name, err);
			goto out_err;
		}
	}

	channel = g_io_channel_unix_new(fd);
	g_io_channel_set_close_on_unref(channel, TRUE);

	server_data->watch = g_io_add_watch(channel,
				G_IO_IN | G_IO_HUP | G_IO_NVAL | G_IO_ERR,
				listener, (gpointer) server_data);
	g_io_channel_unref(channel);

	return 0;

out_err:
	close(fd);
	return err;
}

static gboolean p2p_listener_event(GIOChannel *channel, GIOCondition condition,
				   gpointer user_data);

struct p2p_connect {
	uint32_t adapter_idx;
	uint32_t target_idx;
	guint watch;
	struct near_p2p_driver *driver;
	struct near_ndef_message *ndef;
	near_device_io_cb cb;
};

static gboolean p2p_client_event(GIOChannel *channel, GIOCondition condition,
							gpointer user_data)
{
	struct p2p_data *client_data = user_data;

	DBG("condition 0x%x", condition);

	if (condition & (G_IO_NVAL | G_IO_ERR | G_IO_HUP)) {
		int err;
		struct p2p_data *server_data = client_data->server;

		if (client_data->watch > 0)
			g_source_remove(client_data->watch);
		client_data->watch = 0;

		if (condition & (G_IO_NVAL | G_IO_ERR))
			err = -EIO;
		else
			err = 0;

		if (client_data->driver->close)
			client_data->driver->close(client_data->fd, err,
						client_data->driver->user_data);

		near_error("%s client channel closed",
					client_data->driver->name);

		if (server_data)
			server_data->client_list =
				g_list_remove(server_data->client_list,
								client_data);

		if (client_data->driver->single_connection) {
			server_data->fd = socket(AF_NFC, SOCK_STREAM,
							NFC_SOCKPROTO_LLCP);
			if (server_data->fd > 0)
				__p2p_bind(server_data, p2p_listener_event);
		}

		if (client_data->driver->sock_type == SOCK_STREAM)
			g_free(client_data);

		return FALSE;
	}

	if (client_data->driver->new_client)
		return true;

	return client_data->driver->read(client_data->fd,
						client_data->adapter_idx,
						client_data->target_idx,
						client_data->cb,
						client_data->driver->user_data);

}

static void free_client_data(gpointer data)
{
	struct p2p_data *client_data;

	DBG("");

	client_data = (struct p2p_data *) data;

	if (client_data->driver->close)
		client_data->driver->close(client_data->fd, 0,
						client_data->driver->user_data);

	if (client_data->watch > 0)
		g_source_remove(client_data->watch);

	g_free(client_data);
}

static void free_server_data(gpointer data)
{
	struct p2p_data *server_data;

	DBG("");

	server_data = (struct p2p_data *)data;

	if (server_data->watch > 0)
		g_source_remove(server_data->watch);
	server_data->watch = 0;
	g_list_free_full(server_data->client_list, free_client_data);
	server_data->client_list = NULL;

	DBG("Closing server socket");

	close(server_data->fd);

	g_free(server_data);
}

static gboolean p2p_listener_event(GIOChannel *channel, GIOCondition condition,
							gpointer user_data)
{
	struct sockaddr_nfc_llcp client_addr;
	socklen_t client_addr_len;
	int server_fd, client_fd;
	struct p2p_data *client_data, *server_data = user_data;
	GIOChannel *client_channel;
	struct near_p2p_driver *driver = server_data->driver;

	DBG("condition 0x%x", condition);

	server_fd = g_io_channel_unix_get_fd(channel);

	if (condition & (G_IO_NVAL | G_IO_ERR | G_IO_HUP)) {
		if (server_data->watch > 0)
			g_source_remove(server_data->watch);
		server_data->watch = 0;

		g_list_free_full(server_data->client_list, free_client_data);
		server_data->client_list = NULL;

		near_error("Error with %s server channel", driver->name);

		return FALSE;
	}

	client_addr_len = sizeof(client_addr);
	client_fd = accept(server_fd, (struct sockaddr *) &client_addr,
							&client_addr_len);
	if (client_fd < 0) {
		near_error("accept failed %d", client_fd);

		return FALSE;
	}

	DBG("client dsap %u ssap %u",
		client_addr.dsap, client_addr.ssap);
	DBG("target idx %u", client_addr.target_idx);

	client_data = g_try_malloc0(sizeof(struct p2p_data));
	if (!client_data) {
		close(client_fd);
		return FALSE;
	}

	client_data->driver = server_data->driver;
	client_data->adapter_idx = server_data->adapter_idx;
	client_data->target_idx = client_addr.target_idx;
	client_data->fd = client_fd;
	client_data->cb = server_data->cb;
	client_data->server = server_data;

	client_channel = g_io_channel_unix_new(client_fd);
	g_io_channel_set_close_on_unref(client_channel, TRUE);

	/* This would enable passthru active */
	if (server_data->driver->new_client)
		server_data->driver->new_client(
				client_data->driver->service_name, client_fd,
				server_data->driver->user_data);

	client_data->watch = g_io_add_watch(client_channel,
				G_IO_IN | G_IO_HUP | G_IO_NVAL | G_IO_ERR,
				p2p_client_event,
				client_data);

	g_io_channel_unref(client_channel);

	server_data->client_list = g_list_append(server_data->client_list,
								client_data);

	return !driver->single_connection;
}

static int p2p_connect_blocking(uint32_t adapter_idx, uint32_t target_idx,
				struct near_ndef_message *ndef,
				near_device_io_cb cb,
				struct near_p2p_driver *driver)
{
	int fd, err = 0;
	struct timeval timeout;
	struct sockaddr_nfc_llcp addr;

	DBG("");

	fd = socket(AF_NFC, SOCK_STREAM, NFC_SOCKPROTO_LLCP);
	if (fd < 0)
		return -errno;

	memset(&addr, 0, sizeof(struct sockaddr_nfc_llcp));
	addr.sa_family = AF_NFC;
	addr.dev_idx = adapter_idx;
	addr.target_idx = target_idx;
	addr.nfc_protocol = NFC_PROTO_NFC_DEP;
	addr.service_name_len = strlen(driver->service_name);
	strcpy(addr.service_name, driver->service_name);

	timeout.tv_sec = 8;
	timeout.tv_usec = 0;

	if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout,
			sizeof(timeout)) < 0)
		near_error("Could not set the receive timeout\n");

	if (setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout,
			sizeof(timeout)) < 0)
		near_error("Could not set the send timeout\n");

	err = connect(fd, (struct sockaddr *) &addr,
			sizeof(struct sockaddr_nfc_llcp));
	if (err < 0) {
		near_error("Connect failed  %d", err);
		close(fd);

		return err;
	}

	return fd;
}

static gboolean p2p_push_blocking(gpointer user_data)
{
	struct p2p_connect *conn = user_data;
	int fd, err;

	DBG("");

	fd = p2p_connect_blocking(conn->adapter_idx, conn->target_idx,
				  conn->ndef, conn->cb, conn->driver);
	if (fd < 0) {
		err = fd;
		goto out;
	}

	err = conn->driver->push(fd, conn->adapter_idx, conn->target_idx,
						conn->ndef, conn->cb,
						conn->driver->user_data);

out:
	if (err < 0)
		conn->cb(conn->adapter_idx, conn->target_idx, err);

	g_free(conn->ndef->data);
	g_free(conn->ndef);
	g_free(conn);

	return FALSE;
}

static bool check_nval(GIOChannel *io)
{
	struct pollfd fds;

	memset(&fds, 0, sizeof(fds));
	fds.fd = g_io_channel_unix_get_fd(io);
	fds.events = POLLNVAL;

	if (poll(&fds, 1, 0) > 0 && (fds.revents & POLLNVAL))
		return true;

	return false;
}

static gboolean p2p_connect_event(GIOChannel *channel, GIOCondition condition,
							gpointer user_data)
{
	struct p2p_connect *conn = user_data;
	int err, sk_err, fd;
	socklen_t len = sizeof(sk_err);

	DBG("condition 0x%x", condition);

	if (!conn->driver->push) {
		err = -EOPNOTSUPP;
		goto out;
	}

	fd = g_io_channel_unix_get_fd(channel);

	if ((condition & (G_IO_NVAL | G_IO_ERR | G_IO_HUP)) ||
						check_nval(channel)) {
		near_error("%s connect error", conn->driver->name);

		if (condition & G_IO_HUP) {
			DBG("Trying a blocking connect");

			close(fd);

			g_timeout_add(300, p2p_push_blocking, conn);

			return FALSE;
		}

		err = -EIO;
		goto out;
	}

	if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &sk_err, &len) < 0)
		err = -errno;
	else
		err = -sk_err;
	if (err < 0) {
		near_error("%s Connection error %d",
					conn->driver->name, -err);
		goto out;
	}

	err = conn->driver->push(fd, conn->adapter_idx, conn->target_idx,
					conn->ndef, conn->cb,
					conn->driver->user_data);

out:
	if (err < 0)
		conn->cb(conn->adapter_idx, conn->target_idx, err);

	g_free(conn->ndef->data);
	g_free(conn->ndef);
	g_free(conn);

	return FALSE;
}

static int p2p_bind(struct near_p2p_driver *driver, uint32_t adapter_idx,
							near_device_io_cb cb)
{
	int err, fd;
	struct p2p_data *server_data;
	GIOFunc g_func;

	DBG("");

	if (driver->sock_type == SOCK_DGRAM)
		g_func = p2p_client_event;
	else
		g_func = p2p_listener_event;

	if (driver->sock_type != SOCK_DGRAM &&
					driver->sock_type != SOCK_STREAM) {
		near_error("Undefined socket type for %s ", driver->name);
		return -EINVAL;
	}

	fd = socket(AF_NFC, driver->sock_type, NFC_SOCKPROTO_LLCP);
	if (fd < 0)
		return -errno;

	server_data = g_try_malloc0(sizeof(struct p2p_data));
	if (!server_data) {
		close(fd);
		return -ENOMEM;
	}

	server_data->driver = driver;
	server_data->adapter_idx = adapter_idx;
	server_data->fd = fd;
	server_data->cb = cb;

	err = __p2p_bind(server_data, g_func);
	if (err < 0) {
		g_free(server_data);
		return err;
	}

	server_list = g_list_append(server_list, server_data);

	return 0;
}

static int p2p_listen(uint32_t adapter_idx, near_device_io_cb cb)
{
	int err = -1, bind_err;
	GSList *list;

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

		bind_err = p2p_bind(driver, adapter_idx, cb);
		if (bind_err == 0) {
			err = 0;
		} else if (bind_err == -EPROTONOSUPPORT) {
			near_error("LLCP is not supported");
			return bind_err;
		}
	}

	return err;
}

static int p2p_connect(uint32_t adapter_idx, uint32_t target_idx,
			struct near_ndef_message *ndef,
			near_device_io_cb cb,  struct near_p2p_driver *driver)
{
	int fd, err = 0;
	struct timeval timeout;
	struct sockaddr_nfc_llcp addr;
	GIOChannel *channel;
	GIOCondition cond;
	struct p2p_connect *conn;

	DBG("");

	fd = socket(AF_NFC, SOCK_STREAM, NFC_SOCKPROTO_LLCP);
	if (fd < 0)
		return -errno;

	conn = g_try_malloc0(sizeof(struct p2p_connect));
	if (!conn) {
		close(fd);
		return -ENOMEM;
	}

	conn->driver = driver;
	conn->ndef = ndef;
	conn->cb = cb;
	conn->target_idx = target_idx;
	conn->adapter_idx = adapter_idx;

	channel = g_io_channel_unix_new(fd);
	g_io_channel_set_flags(channel, G_IO_FLAG_NONBLOCK, NULL);

	memset(&addr, 0, sizeof(struct sockaddr_nfc_llcp));
	addr.sa_family = AF_NFC;
	addr.dev_idx = adapter_idx;
	addr.target_idx = target_idx;
	addr.nfc_protocol = NFC_PROTO_NFC_DEP;
	addr.service_name_len = strlen(driver->service_name);
	strcpy(addr.service_name, driver->service_name);

	timeout.tv_sec = 8;
	timeout.tv_usec = 0;

	if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout,
			sizeof(timeout)) < 0)
		near_error("Could not set the receive timeout\n");

	if (setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout,
			sizeof(timeout)) < 0)
		near_error("Could not set the send timeout\n");

	err = connect(fd, (struct sockaddr *) &addr,
			sizeof(struct sockaddr_nfc_llcp));
	if (err < 0 && errno != EINPROGRESS) {
		near_error("Connect failed  %d", errno);
		g_free(conn);
		close(fd);

		return err;
	}

	cond = G_IO_OUT | G_IO_HUP |  G_IO_ERR | G_IO_NVAL;
	conn->watch = g_io_add_watch(channel, cond, p2p_connect_event, conn);
	g_io_channel_unref(channel);

	return fd;
}

static int p2p_push(uint32_t adapter_idx, uint32_t target_idx,
			struct near_ndef_message *ndef, char *service_name,
			near_device_io_cb cb)
{
	int fd;
	GSList *list;

	DBG("");

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

		if (strcmp(driver->service_name, service_name) != 0)
			continue;
		/*
		 * Because of Android's implementation, we have use SNEP for
		 * Handover. So, on Handover session, we try to connect to
		 * the handover service and fallback to SNEP on connect fail.
		 */
		fd = p2p_connect(adapter_idx, target_idx, ndef, cb, driver);
		if (fd > 0)
			return fd;

		if (driver->fallback_service_name)
			return  p2p_push(adapter_idx, target_idx, ndef,
					(char *) driver->fallback_service_name,
					cb);
		else
			return -1;
	}

	return -1;
}

static struct near_device_driver p2p_driver = {
	.priority       = NEAR_DEVICE_PRIORITY_HIGH,
	.listen         = p2p_listen,
	.push		= p2p_push,
};

int near_p2p_register(struct near_p2p_driver *driver)
{
	struct near_p2p_driver *tmp_driver;
	GSList *list = NULL;

	DBG("driver %p name %s service %s", driver, driver->name,
			driver->service_name);

	for (list = driver_list; list; list = list->next) {
		tmp_driver = list->data;
		if (g_strcmp0(tmp_driver->service_name,
						driver->service_name) == 0) {
			near_error("%s already registered",
							driver->service_name);
			return -EALREADY;
		}
	}

	driver_list = g_slist_prepend(driver_list, driver);
	return 0;
}

void near_p2p_unregister(struct near_p2p_driver *driver)
{
	DBG("driver %p name %s", driver, driver->service_name);

	driver_list = g_slist_remove(driver_list, driver);
}

static int p2p_init(void)
{
	DBG("");

	phdc_init();
	npp_init();
	snep_init();
	snep_validation_init();
	llcp_validation_init();
	handover_init();

	return near_device_driver_register(&p2p_driver);
}

static void p2p_exit(void)
{
	DBG("");

	g_list_free_full(server_list, free_server_data);

	llcp_validation_exit();
	snep_exit();
	snep_validation_exit();
	npp_exit();
	phdc_exit();
	handover_exit();

	near_device_driver_unregister(&p2p_driver);
}

NEAR_PLUGIN_DEFINE(p2p, "NFC Forum peer to peer mode support", VERSION,
		NEAR_PLUGIN_PRIORITY_HIGH, p2p_init, p2p_exit)
