/* -*- mode: c; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
/* vi: set ts=8 sw=8 sts=8: */
/*************************************************************************/ /*!
@File
@Codingstyle    LinuxKernel
@Copyright      Copyright (c) Imagination Technologies Ltd. All Rights Reserved
@License        Dual MIT/GPLv2

The contents of this file are subject to the MIT license as set out below.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

Alternatively, the contents of this file may be used under the terms of
the GNU General Public License Version 2 ("GPL") in which case the provisions
of GPL are applicable instead of those above.

If you wish to allow use of your version of this file only under the terms of
GPL, and not to allow others to use your version of this file under the terms
of the MIT license, indicate your decision by deleting the provisions above
and replace them with the notice and other provisions required by GPL as set
out in the file called "GPL-COPYING" included in this distribution. If you do
not delete the provisions above, a recipient may use your version of this file
under the terms of either the MIT license or GPL.

This License is also included in this distribution in the file called
"MIT-COPYING".

EXCEPT AS OTHERWISE STATED IN A NEGOTIATED AGREEMENT: (A) THE SOFTWARE IS
PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT; AND (B) IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ /**************************************************************************/

#include <linux/mutex.h>
#include <linux/list.h>
#include <linux/random.h>
#include <linux/slab.h>
#include <linux/atomic.h>
#include <linux/version.h>
#if (LINUX_VERSION_CODE >= KERNEL_VERSION(4, 6, 0))
#include <uapi/linux/if_macsec.h>
#endif
#include "drm_nulldisp_netlink.h"

struct nlpvrdpy {
	atomic_t connected;
	struct net *net;
	u32 dst_portid;
	struct drm_device *dev;
	nlpvrdpy_disconnect_cb disconnect_cb;
	void *disconnect_cb_data;
	nlpvrdpy_flipped_cb flipped_cb;
	void *flipped_cb_data;
	nlpvrdpy_copied_cb copied_cb;
	void *copied_cb_data;
	struct mutex mutex;
	struct list_head nl_list;
};
#define NLPVRDPY_MINOR(nlpvrdpy) ((unsigned)((nlpvrdpy)->dev->primary->index))

enum nlpvrdpy_attr {
	__NLPVRDPY_ATTR_INVALID,
	NLPVRDPY_ATTR_NAME,
	NLPVRDPY_ATTR_MINOR,
	NLPVRDPY_ATTR_MMAP_OFFSET,
	NLPVRDPY_ATTR_SIZE,
	NLPVRDPY_ATTR_WIDTH,
	NLPVRDPY_ATTR_HEIGHT,
	NLPVRDPY_ATTR_BITSPP,
	NLPVRDPY_ATTR_PIXFMT,
	__NLPVRDPY_ATTR_MAX
};
#define NLPVRDPY_ATTR_MAX  (__NLPVRDPY_ATTR_MAX - 1)

enum nlpvrdpy_cmd {
	__NLPVRDPY_CMD_INVALID,
	NLPVRDPY_CMD_CONNECT,
	NLPVRDPY_CMD_CONNECTED,
	NLPVRDPY_CMD_DISCONNECT,
	NLPVRDPY_CMD_FLIP,
	NLPVRDPY_CMD_FLIPPED,
	NLPVRDPY_CMD_COPY,
	NLPVRDPY_CMD_COPIED,
	__NLPVRDPY_CMD_MAX
};
#define NLPVRDPY_CMD_MAX (__NLPVRDPY_CMD_MAX - 1)

/* Command internal flags */
#define	NLPVRDPY_CIF_NLPVRDPY_NOT_CONNECTED	0x00000001
#define	NLPVRDPY_CIF_NLPVRDPY			0x00000002

static atomic_t nlpvrdpy_started = ATOMIC_INIT(0);
static DECLARE_COMPLETION(nlpvrdpy_wait_for_start);

static LIST_HEAD(nlpvrdpy_list);
static DEFINE_MUTEX(nlpvrdpy_list_mutex);

static inline void nlpvrdpy_lock(struct nlpvrdpy *nlpvrdpy)
{
	mutex_lock(&nlpvrdpy->mutex);
}

static inline void nlpvrdpy_unlock(struct nlpvrdpy *nlpvrdpy)
{
	mutex_unlock(&nlpvrdpy->mutex);
}

struct nlpvrdpy *nlpvrdpy_create(struct drm_device *dev,
					nlpvrdpy_disconnect_cb disconnect_cb,
					void *disconnect_cb_data,
					nlpvrdpy_flipped_cb flipped_cb,
					void *flipped_cb_data,
					nlpvrdpy_copied_cb copied_cb,
					void *copied_cb_data)
{
	struct nlpvrdpy *nlpvrdpy = kzalloc(sizeof(*nlpvrdpy), GFP_KERNEL);

	if (!nlpvrdpy)
		return NULL;

	mutex_init(&nlpvrdpy->mutex);
	INIT_LIST_HEAD(&nlpvrdpy->nl_list);

	atomic_set(&nlpvrdpy->connected, 0);

	nlpvrdpy->dev = dev;
	nlpvrdpy->disconnect_cb = disconnect_cb;
	nlpvrdpy->disconnect_cb_data = disconnect_cb_data;
	nlpvrdpy->flipped_cb = flipped_cb;
	nlpvrdpy->flipped_cb_data = flipped_cb_data;
	nlpvrdpy->copied_cb = copied_cb;
	nlpvrdpy->copied_cb_data = copied_cb_data;

	mutex_lock(&nlpvrdpy_list_mutex);
	list_add_tail(&nlpvrdpy->nl_list, &nlpvrdpy_list);
	mutex_unlock(&nlpvrdpy_list_mutex);

	return nlpvrdpy;
}

void nlpvrdpy_destroy(struct nlpvrdpy *nlpvrdpy)
{
	if (!nlpvrdpy)
		return;

	mutex_lock(&nlpvrdpy_list_mutex);
	nlpvrdpy_lock(nlpvrdpy);
	list_del(&nlpvrdpy->nl_list);
	nlpvrdpy_unlock(nlpvrdpy);
	mutex_unlock(&nlpvrdpy_list_mutex);

	mutex_destroy(&nlpvrdpy->mutex);

	kfree(nlpvrdpy);
}

static struct nlpvrdpy *nlpvrdpy_lookup(u32 minor)
{
	struct nlpvrdpy *nlpvrdpy = NULL;
	struct nlpvrdpy *iter;

	mutex_lock(&nlpvrdpy_list_mutex);
	list_for_each_entry(iter, &nlpvrdpy_list, nl_list) {
		if (NLPVRDPY_MINOR(iter) == minor) {
			nlpvrdpy = iter;
			nlpvrdpy_lock(nlpvrdpy);
			break;
		}
	}
	mutex_unlock(&nlpvrdpy_list_mutex);

	return nlpvrdpy;
}

static int nlpvrdpy_pre_cmd(const struct genl_ops *ops,
				struct sk_buff *skb,
				struct genl_info *info)
{
	struct nlattr **attrs = info->attrs;
	struct nlpvrdpy *nlpvrdpy = NULL;
	int ret;

	if (ops->internal_flags & NLPVRDPY_CIF_NLPVRDPY_NOT_CONNECTED) {
		if (!(ops->flags & GENL_ADMIN_PERM))
			return -EINVAL;

		if (!atomic_read(&nlpvrdpy_started))
			wait_for_completion(&nlpvrdpy_wait_for_start);
	}

	if (ops->internal_flags & (NLPVRDPY_CIF_NLPVRDPY_NOT_CONNECTED |
					NLPVRDPY_CIF_NLPVRDPY)) {
		u32 minor;

		if (!attrs[NLPVRDPY_ATTR_MINOR])
			return -EINVAL;

		minor = nla_get_u32(attrs[NLPVRDPY_ATTR_MINOR]);

		nlpvrdpy = nlpvrdpy_lookup(minor);
		if (!nlpvrdpy)
			return -ENODEV;

		if (ops->internal_flags & NLPVRDPY_CIF_NLPVRDPY) {
			if (!atomic_read(&nlpvrdpy->connected)) {
				ret = -ENOTCONN;
				goto err_unlock;
			}
			if ((nlpvrdpy->net != genl_info_net(info)) ||
				(nlpvrdpy->dst_portid != info->snd_portid)) {
				ret = -EPROTO;
				goto err_unlock;
			}
		}

		info->user_ptr[0] = nlpvrdpy;
	}

	ret = 0;

err_unlock:
	nlpvrdpy_unlock(nlpvrdpy);
	return ret;
}

static void nlpvrdpy_post_cmd(const struct genl_ops *ops,
				struct sk_buff *skb,
				struct genl_info *info)
{
}

static struct genl_family nlpvrdpy_family = {
	.id = GENL_ID_GENERATE,
	.name = "nlpvrdpy",
	.version = 1,
	.maxattr = NLPVRDPY_ATTR_MAX,
	.pre_doit = &nlpvrdpy_pre_cmd,
	.post_doit = &nlpvrdpy_post_cmd
};

/* Must be called with the struct nlpvrdpy mutex held */
static int nlpvrdpy_send_msg_locked(struct nlpvrdpy *nlpvrdpy,
					struct sk_buff *msg)
{
	int err;

	if (atomic_read(&nlpvrdpy->connected)) {
		err = genlmsg_unicast(nlpvrdpy->net, msg, nlpvrdpy->dst_portid);
		if (err == -ECONNREFUSED)
			atomic_set(&nlpvrdpy->connected, 0);
	} else {
		err = -ENOTCONN;
		nlmsg_free(msg);
	}

	return err;
}

static int nlpvrdpy_send_msg(struct nlpvrdpy *nlpvrdpy, struct sk_buff *msg)
{
	int err;

	nlpvrdpy_lock(nlpvrdpy);
	err = nlpvrdpy_send_msg_locked(nlpvrdpy, msg);
	nlpvrdpy_unlock(nlpvrdpy);

	return err;
}

void nlpvrdpy_send_disconnect(struct nlpvrdpy *nlpvrdpy)
{
	struct sk_buff *msg;
	void *hdr;
	int err;

	if (!atomic_read(&nlpvrdpy->connected))
		return;

	msg = genlmsg_new(NLMSG_GOODSIZE, GFP_KERNEL);
	if (!msg)
		return;

	hdr = genlmsg_put(msg, nlpvrdpy->dst_portid, 0,
				&nlpvrdpy_family, 0, NLPVRDPY_CMD_DISCONNECT);
	if (!hdr) 
		goto err_msg_free;

	err = nla_put_u32(msg, NLPVRDPY_ATTR_MINOR, NLPVRDPY_MINOR(nlpvrdpy));
	if (err)
		goto err_msg_free;

	genlmsg_end(msg, hdr);

	nlpvrdpy_lock(nlpvrdpy);

	(void) nlpvrdpy_send_msg_locked(nlpvrdpy, msg);

	atomic_set(&nlpvrdpy->connected, 0);
	nlpvrdpy->net = 0;
	nlpvrdpy->dst_portid = 0;

	nlpvrdpy_unlock(nlpvrdpy);

	return;

err_msg_free:
	nlmsg_free(msg);
}

static int nlpvrdpy_put_fb_attributes(struct sk_buff *msg,	
					struct drm_framebuffer *fb,
					struct nlpvrdpy *nlpvrdpy,
					u64 mmap_offset,
					u64 size)
{
	int err;

	err = nla_put_u32(msg, NLPVRDPY_ATTR_MINOR, NLPVRDPY_MINOR(nlpvrdpy));
	if (err)
		return err;
#if (LINUX_VERSION_CODE < KERNEL_VERSION(4, 7, 0))
	err = nla_put_u64(msg, NLPVRDPY_ATTR_MMAP_OFFSET, mmap_offset);
	if (err)
		return err;

	err = nla_put_u64(msg, NLPVRDPY_ATTR_SIZE, size);
	if (err)
		return err;
#else
	err = nla_put_u64_64bit(msg, NLPVRDPY_ATTR_MMAP_OFFSET, mmap_offset, MACSEC_RXSC_STATS_ATTR_PAD);
	if (err)
		return err;

	err = nla_put_u64_64bit(msg, NLPVRDPY_ATTR_SIZE, size, MACSEC_RXSC_STATS_ATTR_PAD);
	if (err)
		return err;
#endif
	err = nla_put_u32(msg, NLPVRDPY_ATTR_WIDTH, fb->width);
	if (err)
		return err;

	err = nla_put_u32(msg, NLPVRDPY_ATTR_HEIGHT, fb->height);
	if (err)
		return err;

	err = nla_put_u32(msg, NLPVRDPY_ATTR_BITSPP, fb->bits_per_pixel);
	if (err)
		return err;

	err = nla_put_u32(msg, NLPVRDPY_ATTR_PIXFMT, fb->pixel_format);
	if (err)
		return err;

	return 0;
}

int nlpvrdpy_send_flip(struct nlpvrdpy *nlpvrdpy,
			struct drm_framebuffer *fb,
			u64 mmap_offset,
			u64 size)
{
	struct sk_buff *msg;
	void *hdr;
	int err;

	if (!atomic_read(&nlpvrdpy->connected))
		return -ENOTCONN;

	msg = genlmsg_new(NLMSG_GOODSIZE, GFP_KERNEL);
	if (!msg)
		return -ENOMEM;

	hdr = genlmsg_put(msg, nlpvrdpy->dst_portid, 0,
				&nlpvrdpy_family, 0, NLPVRDPY_CMD_FLIP);
	if (!hdr) {
		err = -ENOMEM;
		goto err_msg_free;
	}

	err = nlpvrdpy_put_fb_attributes(msg, fb, nlpvrdpy, mmap_offset, size);
	if (err)
		goto err_msg_free;

	genlmsg_end(msg, hdr);

	return nlpvrdpy_send_msg(nlpvrdpy, msg);

err_msg_free:
	nlmsg_free(msg);
	return err;
}

int nlpvrdpy_send_copy(struct nlpvrdpy *nlpvrdpy,
			struct drm_framebuffer *fb,
			u64 mmap_offset,
			u64 size)
{
	struct sk_buff *msg;
	void *hdr;
	int err;

	if (!atomic_read(&nlpvrdpy->connected))
		return -ENOTCONN;

	msg = genlmsg_new(NLMSG_GOODSIZE, GFP_KERNEL);
	if (!msg)
		return -ENOMEM;

	hdr = genlmsg_put(msg, nlpvrdpy->dst_portid, 0,
				&nlpvrdpy_family, 0, NLPVRDPY_CMD_COPY);
	if (!hdr) {
		err = -ENOMEM;
		goto err_msg_free;
	}

	err = nlpvrdpy_put_fb_attributes(msg, fb, nlpvrdpy, mmap_offset, size);
	if (err)
		goto err_msg_free;

	genlmsg_end(msg, hdr);

	return nlpvrdpy_send_msg(nlpvrdpy, msg);

err_msg_free:
	nlmsg_free(msg);
	return err;
}

static struct nla_policy nlpvrdpy_policy[NLPVRDPY_ATTR_MAX + 1] = {
	[NLPVRDPY_ATTR_NAME] = { .type = NLA_STRING },
	[NLPVRDPY_ATTR_MINOR] = { .type = NLA_U32 },
	[NLPVRDPY_ATTR_MMAP_OFFSET] = { .type = NLA_U64 },
	[NLPVRDPY_ATTR_SIZE] = { .type = NLA_U64 },
	[NLPVRDPY_ATTR_WIDTH] = { .type = NLA_U32 },
	[NLPVRDPY_ATTR_HEIGHT] = { .type = NLA_U32 },
	[NLPVRDPY_ATTR_BITSPP] = { .type = NLA_U32 }
};

static int nlpvrdpy_cmd_connect(struct sk_buff *skb, struct genl_info *info)
{
	struct nlpvrdpy *nlpvrdpy = info->user_ptr[0];
	struct sk_buff *msg;
	void *hdr;
	int err;

	msg = genlmsg_new(NLMSG_GOODSIZE, GFP_KERNEL);
	if (!msg)
		return -ENOMEM;

	hdr = genlmsg_put_reply(msg, info, &nlpvrdpy_family,
						0, NLPVRDPY_CMD_CONNECTED);
	if (!hdr) {
		err = -ENOMEM;
		goto err_msg_free;
	}
	
	err = nla_put_string(msg, NLPVRDPY_ATTR_NAME,
				nlpvrdpy->dev->driver->name);
	if (err)
		goto err_msg_free;

	genlmsg_end(msg, hdr);

	err = genlmsg_reply(msg, info);

	if (!err) {
		nlpvrdpy_lock(nlpvrdpy);

		nlpvrdpy->net = genl_info_net(info);
		nlpvrdpy->dst_portid = info->snd_portid;
		atomic_set(&nlpvrdpy->connected, 1);

		nlpvrdpy_unlock(nlpvrdpy);
	}

	return err;

err_msg_free:
	nlmsg_free(msg);
	return err;
}

static int nlpvrdpy_cmd_disconnect(struct sk_buff *skb, struct genl_info *info)
{
	struct nlpvrdpy *nlpvrdpy = info->user_ptr[0];

	atomic_set(&nlpvrdpy->connected, 0);

	if (nlpvrdpy->disconnect_cb)
		nlpvrdpy->disconnect_cb(nlpvrdpy->disconnect_cb_data);

	return 0;
}

static int nlpvrdpy_cmd_flipped(struct sk_buff *skb, struct genl_info *info)
{
	struct nlpvrdpy *nlpvrdpy = info->user_ptr[0];

	return (nlpvrdpy->flipped_cb) ?
			nlpvrdpy->flipped_cb(nlpvrdpy->flipped_cb_data) :
			0;
}

static int nlpvrdpy_cmd_copied(struct sk_buff *skb, struct genl_info *info)
{
	struct nlpvrdpy *nlpvrdpy = info->user_ptr[0];

	return (nlpvrdpy->copied_cb) ?
			nlpvrdpy->copied_cb(nlpvrdpy->copied_cb_data) :
			0;
}

static struct genl_ops nlpvrdpy_ops[] = {
	{
		.cmd = NLPVRDPY_CMD_CONNECT,
		.policy = nlpvrdpy_policy,
		.doit = nlpvrdpy_cmd_connect,
		.flags = GENL_ADMIN_PERM,
		.internal_flags = NLPVRDPY_CIF_NLPVRDPY_NOT_CONNECTED
	},
	{
		.cmd = NLPVRDPY_CMD_DISCONNECT,
		.policy = nlpvrdpy_policy,
		.doit = nlpvrdpy_cmd_disconnect,
		.flags = 0,
		.internal_flags = NLPVRDPY_CIF_NLPVRDPY
	},
	{
		.cmd = NLPVRDPY_CMD_FLIPPED,
		.policy = nlpvrdpy_policy,
		.doit = nlpvrdpy_cmd_flipped,
		.flags = 0,
		.internal_flags = NLPVRDPY_CIF_NLPVRDPY
	},
	{
		.cmd = NLPVRDPY_CMD_COPIED,
		.policy = nlpvrdpy_policy,
		.doit = nlpvrdpy_cmd_copied,
		.flags = 0,
		.internal_flags = NLPVRDPY_CIF_NLPVRDPY
	}
};

int nlpvrdpy_register(void)
{
	return genl_register_family_with_ops(&nlpvrdpy_family, nlpvrdpy_ops);
}

void nlpvrdpy_start(void)
{
	if (!atomic_read(&nlpvrdpy_started)) {
		atomic_set(&nlpvrdpy_started, 1);
		complete_all(&nlpvrdpy_wait_for_start);
	}
}

int nlpvrdpy_unregister(void)
{
	nlpvrdpy_start();

	return genl_unregister_family(&nlpvrdpy_family);
}
