/*
 *
 *  Embedded Linux library
 *
 *  Copyright (C) 2011-2014  Intel Corporation. All rights reserved.
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library 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
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; 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 <sys/socket.h>
#include <linux/netlink.h>

#include "useful.h"
#include "hashmap.h"
#include "queue.h"
#include "io.h"
#include "netlink-private.h"
#include "netlink.h"
#include "private.h"

struct command {
	unsigned int id;
	uint32_t seq;
	uint32_t len;
	l_netlink_command_func_t handler;
	l_netlink_destroy_func_t destroy;
	void *user_data;
};

struct notify {
	uint32_t group;
	l_netlink_notify_func_t handler;
	l_netlink_destroy_func_t destroy;
	void *user_data;
};

struct l_netlink {
	uint32_t pid;
	struct l_io *io;
	uint32_t next_seq;
	struct l_queue *command_queue;
	struct l_hashmap *command_pending;
	struct l_hashmap *command_lookup;
	unsigned int next_command_id;
	struct l_hashmap *notify_groups;
	struct l_hashmap *notify_lookup;
	unsigned int next_notify_id;
	l_netlink_debug_func_t debug_handler;
	l_netlink_destroy_func_t debug_destroy;
	void *debug_data;
};

static void destroy_command(void *data)
{
	struct command *command = data;

	if (command->destroy)
		command->destroy(command->user_data);

	l_free(command);
}

static void destroy_notify(void *data)
{
	struct notify *notify = data;

	if (notify->destroy)
		notify->destroy(notify->user_data);

	l_free(notify);
}

static void destroy_notify_group(void *data)
{
	struct l_hashmap *notify_list = data;

	l_hashmap_destroy(notify_list, destroy_notify);
}

static bool can_write_data(struct l_io *io, void *user_data)
{
	struct l_netlink *netlink = user_data;
	struct command *command;
	struct sockaddr_nl addr;
	const void *data;
	ssize_t written;
	int sk;

	command = l_queue_pop_head(netlink->command_queue);
	if (!command)
		return false;

	sk = l_io_get_fd(io);

	memset(&addr, 0, sizeof(addr));
	addr.nl_family = AF_NETLINK;
	addr.nl_pid = 0;

	data = ((void *) command) + NLMSG_ALIGN(sizeof(struct command));

	written = sendto(sk, data, command->len, 0,
				(struct sockaddr *) &addr, sizeof(addr));
	if (written < 0 || (uint32_t) written != command->len) {
		l_hashmap_remove(netlink->command_lookup,
					L_UINT_TO_PTR(command->id));
		destroy_command(command);
		return true;
	}

	l_util_hexdump(false, data, command->len,
				netlink->debug_handler, netlink->debug_data);

	l_hashmap_insert(netlink->command_pending,
				L_UINT_TO_PTR(command->seq), command);

	return l_queue_length(netlink->command_queue) > 0;
}

static void do_notify(const void *key, void *value, void *user_data)
{
	struct nlmsghdr *nlmsg = user_data;
	struct notify *notify = value;

	if (notify->handler) {
		notify->handler(nlmsg->nlmsg_type, NLMSG_DATA(nlmsg),
			nlmsg->nlmsg_len - NLMSG_HDRLEN, notify->user_data);
	}
}

static void process_broadcast(struct l_netlink *netlink, uint32_t group,
						struct nlmsghdr *nlmsg)
{
	struct l_hashmap *notify_list;

	notify_list = l_hashmap_lookup(netlink->notify_groups,
						L_UINT_TO_PTR(group));
	if (!notify_list)
		return;

	l_hashmap_foreach(notify_list, do_notify, nlmsg);
}

static void process_message(struct l_netlink *netlink, struct nlmsghdr *nlmsg)
{
	const void *data = nlmsg;
	struct command *command;

	command = l_hashmap_remove(netlink->command_pending,
					L_UINT_TO_PTR(nlmsg->nlmsg_seq));
	if (!command)
		return;

	if (!command->handler)
		goto done;

	if (nlmsg->nlmsg_type < NLMSG_MIN_TYPE) {
		const struct nlmsgerr *err;

		switch (nlmsg->nlmsg_type) {
		case NLMSG_ERROR:
			err = data + NLMSG_HDRLEN;

			command->handler(err->error, 0, NULL, 0,
							command->user_data);
			break;
		}
	} else {
		command->handler(0, nlmsg->nlmsg_type, data + NLMSG_HDRLEN,
					nlmsg->nlmsg_len - NLMSG_HDRLEN,
					command->user_data);
	}

done:
	l_hashmap_remove(netlink->command_lookup, L_UINT_TO_PTR(command->id));

	destroy_command(command);
}

static void process_multi(struct l_netlink *netlink, struct nlmsghdr *nlmsg)
{
	const void *data = nlmsg;
	struct command *command;

	if (nlmsg->nlmsg_type < NLMSG_MIN_TYPE) {
		command = l_hashmap_remove(netlink->command_pending,
					L_UINT_TO_PTR(nlmsg->nlmsg_seq));
		if (!command)
			return;

		l_hashmap_remove(netlink->command_lookup,
					L_UINT_TO_PTR(command->id));

		destroy_command(command);
	} else {
		command = l_hashmap_lookup(netlink->command_pending,
					L_UINT_TO_PTR(nlmsg->nlmsg_seq));
		if (!command)
			return;

		if (!command->handler)
			return;

		command->handler(0, nlmsg->nlmsg_type, data + NLMSG_HDRLEN,
					nlmsg->nlmsg_len - NLMSG_HDRLEN,
					command->user_data);
	}
}

static bool can_read_data(struct l_io *io, void *user_data)
{
	struct l_netlink *netlink = user_data;
	struct cmsghdr *cmsg;
	struct msghdr msg;
	struct iovec iov;
	struct nlmsghdr *nlmsg;
	unsigned char buffer[4096];
	unsigned char control[32];
	uint32_t group = 0;
	ssize_t len;
	int sk;

	memset(buffer, 0, sizeof(buffer));
	memset(control, 0, sizeof(control));

	sk = l_io_get_fd(io);

	iov.iov_base = buffer;
	iov.iov_len = sizeof(buffer);

	memset(&msg, 0, sizeof(msg));
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;
	msg.msg_control = control;
	msg.msg_controllen = sizeof(control);

	len = recvmsg(sk, &msg, 0);
	if (len < 0)
		return false;

	l_util_hexdump(true, buffer, len, netlink->debug_handler,
						netlink->debug_data);

	for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL;
					cmsg = CMSG_NXTHDR(&msg, cmsg)) {
		struct nl_pktinfo *pktinfo;

		if (cmsg->cmsg_level != SOL_NETLINK)
			continue;

		if (cmsg->cmsg_type != NETLINK_PKTINFO)
			continue;

		pktinfo = (void *) CMSG_DATA(cmsg);

		group = pktinfo->group;
	}

	for (nlmsg = iov.iov_base; NLMSG_OK(nlmsg, (uint32_t) len);
					nlmsg = NLMSG_NEXT(nlmsg, len)) {
		if (group > 0) {
			process_broadcast(netlink, group, nlmsg);
			continue;
		}

		if (nlmsg->nlmsg_pid != netlink->pid)
			continue;

		if (nlmsg->nlmsg_flags & NLM_F_MULTI)
			process_multi(netlink, nlmsg);
		else
			process_message(netlink, nlmsg);
	}

	return true;
}

static int create_netlink_socket(int protocol, uint32_t *pid)
{
	struct sockaddr_nl addr;
	socklen_t addrlen = sizeof(addr);
	int sk, pktinfo = 1;

	sk = socket(PF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK,
								protocol);
	if (sk < 0)
		return -1;

	memset(&addr, 0, sizeof(addr));
	addr.nl_family = AF_NETLINK;

	if (bind(sk, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
		close(sk);
		return -1;
	}

	if (getsockname(sk, (struct sockaddr *) &addr, &addrlen) < 0) {
		close(sk);
		return -1;
	}

	if (setsockopt(sk, SOL_NETLINK, NETLINK_PKTINFO,
					&pktinfo, sizeof(pktinfo)) < 0) {
		close(sk);
		return -1;
	}

	if (pid)
		*pid = addr.nl_pid;

	return sk;
}

LIB_EXPORT struct l_netlink *l_netlink_new(int protocol)
{
	struct l_netlink *netlink;
	int sk;

	netlink = l_new(struct l_netlink, 1);

	netlink->next_seq = 1;
	netlink->next_command_id = 1;
	netlink->next_notify_id = 1;

	sk = create_netlink_socket(protocol, &netlink->pid);
	if (sk < 0) {
		l_free(netlink);
		return NULL;
	}

	netlink->io = l_io_new(sk);
	if (!netlink->io) {
		close(sk);
		l_free(netlink);
		return NULL;
	}

	l_io_set_close_on_destroy(netlink->io, true);

	l_io_set_read_handler(netlink->io, can_read_data, netlink, NULL);

	netlink->command_queue = l_queue_new();
	netlink->command_pending = l_hashmap_new();
	netlink->command_lookup = l_hashmap_new();

	netlink->notify_groups = l_hashmap_new();
	netlink->notify_lookup = l_hashmap_new();

	return netlink;
}

LIB_EXPORT void l_netlink_destroy(struct l_netlink *netlink)
{
	if (unlikely(!netlink))
		return;

	l_hashmap_destroy(netlink->notify_lookup, NULL);
	l_hashmap_destroy(netlink->notify_groups, destroy_notify_group);

	l_queue_destroy(netlink->command_queue, NULL);
	l_hashmap_destroy(netlink->command_pending, NULL);
	l_hashmap_destroy(netlink->command_lookup, destroy_command);

	l_io_destroy(netlink->io);

	l_free(netlink);
}

LIB_EXPORT unsigned int l_netlink_send(struct l_netlink *netlink,
			uint16_t type, uint16_t flags, const void *data,
			uint32_t len, l_netlink_command_func_t function,
			void *user_data, l_netlink_destroy_func_t destroy)
{
	struct command *command;
	struct nlmsghdr *nlmsg;
	size_t size;

	if (unlikely(!netlink))
		return 0;

	if (!netlink->command_queue || !netlink->command_pending ||
						!netlink->command_lookup)
		return 0;

	if (flags & 0xff)
		return 0;

	if (function)
		flags |= NLM_F_ACK;

	size = NLMSG_ALIGN(sizeof(struct command)) +
					NLMSG_HDRLEN + NLMSG_ALIGN(len);

	command = l_malloc(size);

	memset(command, 0, size);
	command->handler = function;
	command->destroy = destroy;
	command->user_data = user_data;

	command->id = netlink->next_command_id;

	if (!l_hashmap_insert(netlink->command_lookup,
					L_UINT_TO_PTR(command->id), command))
		goto free_command;

	command->seq = netlink->next_seq++;
	command->len = NLMSG_HDRLEN + NLMSG_ALIGN(len);

	nlmsg = ((void *) command) + NLMSG_ALIGN(sizeof(struct command));

	nlmsg->nlmsg_len = command->len;
	nlmsg->nlmsg_type = type;
	nlmsg->nlmsg_flags = NLM_F_REQUEST | flags;
	nlmsg->nlmsg_seq = command->seq;
	nlmsg->nlmsg_pid = netlink->pid;

	if (data && len > 0)
		memcpy(((void *) nlmsg) + NLMSG_HDRLEN, data, len);

	l_queue_push_tail(netlink->command_queue, command);

	l_io_set_write_handler(netlink->io, can_write_data, netlink, NULL);

	netlink->next_command_id++;

	return command->id;

free_command:
	l_free(command);

	return 0;
}

LIB_EXPORT bool l_netlink_cancel(struct l_netlink *netlink, unsigned int id)
{
	struct command *command;

	if (unlikely(!netlink || !id))
		return false;

	if (!netlink->command_queue || !netlink->command_pending ||
						!netlink->command_lookup)
		return false;

	command = l_hashmap_remove(netlink->command_lookup, L_UINT_TO_PTR(id));
	if (!command)
		return false;

	if (!l_queue_remove(netlink->command_queue, command)) {
		l_hashmap_remove(netlink->command_pending,
					L_UINT_TO_PTR(command->seq));
	}

	destroy_command(command);

	return true;
}

static bool add_membership(struct l_netlink *netlink, uint32_t group)
{
	int sk, value = group;

	sk = l_io_get_fd(netlink->io);

	if (setsockopt(sk, SOL_NETLINK, NETLINK_ADD_MEMBERSHIP,
						&value, sizeof(value)) < 0)
		return false;

	return true;
}

static bool drop_membership(struct l_netlink *netlink, uint32_t group)
{
	int sk, value = group;

	sk = l_io_get_fd(netlink->io);

	if (setsockopt(sk, SOL_NETLINK, NETLINK_DROP_MEMBERSHIP,
						&value, sizeof(value)) < 0)
		return false;

	return true;
}

LIB_EXPORT unsigned int l_netlink_register(struct l_netlink *netlink,
			uint32_t group, l_netlink_notify_func_t function,
			void *user_data, l_netlink_destroy_func_t destroy)
{
	struct l_hashmap *notify_list;
	struct notify *notify;
	unsigned int id;

	if (unlikely(!netlink))
		return 0;

	if (!netlink->notify_groups || !netlink->notify_lookup)
		return 0;

	notify_list = l_hashmap_lookup(netlink->notify_groups,
						L_UINT_TO_PTR(group));
	if (!notify_list) {
		notify_list = l_hashmap_new();
		if (!notify_list)
			return 0;

		if (!l_hashmap_insert(netlink->notify_groups,
					L_UINT_TO_PTR(group), notify_list)) {
			l_hashmap_destroy(notify_list, NULL);
			return 0;
		}
	}

	notify = l_new(struct notify, 1);

	notify->group = group;
	notify->handler = function;
	notify->destroy = destroy;
	notify->user_data = user_data;

	id = netlink->next_notify_id;

	if (!l_hashmap_insert(netlink->notify_lookup,
					L_UINT_TO_PTR(id), notify_list))
		goto free_notify;

	if (!l_hashmap_insert(notify_list, L_UINT_TO_PTR(id), notify))
		goto remove_lookup;

	if (l_hashmap_size(notify_list) == 1) {
		if (!add_membership(netlink, notify->group))
			goto remove_notify;
	}

	netlink->next_notify_id++;

	return id;

remove_notify:
	l_hashmap_remove(notify_list, L_UINT_TO_PTR(id));

remove_lookup:
	l_hashmap_remove(netlink->notify_lookup, L_UINT_TO_PTR(id));

free_notify:
	l_free(notify);

	return 0;
}

LIB_EXPORT bool l_netlink_unregister(struct l_netlink *netlink, unsigned int id)
{
	struct l_hashmap *notify_list;
	struct notify *notify;

	if (unlikely(!netlink || !id))
		return false;

	if (!netlink->notify_groups || !netlink->notify_lookup)
		return false;

	notify_list = l_hashmap_remove(netlink->notify_lookup,
						L_UINT_TO_PTR(id));
	if (!notify_list)
		return false;

	notify = l_hashmap_remove(notify_list, L_UINT_TO_PTR(id));
	if (!notify)
		return false;

	if (l_hashmap_size(notify_list) == 0)
		drop_membership(netlink, notify->group);

	destroy_notify(notify);

	return true;
}

LIB_EXPORT bool l_netlink_set_debug(struct l_netlink *netlink,
			l_netlink_debug_func_t function,
			void *user_data, l_netlink_destroy_func_t destroy)
{
	if (unlikely(!netlink))
		return false;

	if (netlink->debug_destroy)
		netlink->debug_destroy(netlink->debug_data);

	netlink->debug_handler = function;
	netlink->debug_destroy = destroy;
	netlink->debug_data = user_data;

	/* l_io_set_debug(netlink->io, function, user_data, NULL); */

	return true;
}
