/*
 * Copyright (c) 2016 Cray Inc. All rights reserved.
 * Copyright (c) 2017 Los Alamos National Security, LLC.
 *                    All rights reserved.
 *
 * This software is available to you under a choice of one of two
 * licenses.  You may choose to be licensed under the terms of the GNU
 * General Public License (GPL) Version 2, available from the file
 * COPYING in the main directory of this source tree, or the
 * BSD license below:
 *
 *     Redistribution and use in source and binary forms, with or
 *     without modification, are permitted provided that the following
 *     conditions are met:
 *
 *      - Redistributions of source code must retain the above
 *        copyright notice, this list of conditions and the following
 *        disclaimer.
 *
 *      - Redistributions in binary form must reproduce the above
 *        copyright notice, this list of conditions and the following
 *        disclaimer in the documentation and/or other materials
 *        provided with the distribution.
 *
 * 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. 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 "gnix_mr_notifier.h"

#if HAVE_KDREG

struct gnix_mr_notifier global_mr_not;

static inline int
notifier_verify_stuff(struct gnix_mr_notifier *mrn) {
	/* Can someone confirm that these values are POSIX so we can
	 * be less pedantic? */
	if (mrn->fd == STDIN_FILENO ||
	    mrn->fd == STDOUT_FILENO ||
	    mrn->fd == STDERR_FILENO ||
	    mrn->fd < 0) {
		// Be quiet here
		return -FI_EBADF;
	}

	if (mrn->cntr == NULL) {
		// Be quiet here
		return -FI_ENODATA;
	}

	return FI_SUCCESS;
}

int
_gnix_notifier_init(void)
{
	global_mr_not.fd = -1;
	global_mr_not.cntr = NULL;
	ofi_spin_init(&global_mr_not.lock);
	global_mr_not.ref_cnt = 0;

	return FI_SUCCESS;
}

int
_gnix_notifier_open(struct gnix_mr_notifier **mrn)
{
	int ret = FI_SUCCESS;
	int kdreg_fd, ret_errno;
        kdreg_get_user_delta_args_t get_user_delta_args;

	ofi_spin_lock(&global_mr_not.lock);

	if (!global_mr_not.ref_cnt) {
		kdreg_fd = open(KDREG_DEV, O_RDWR | O_NONBLOCK);
		if (kdreg_fd < 0) {
			ret_errno = errno;
			GNIX_WARN(FI_LOG_MR,
				  "kdreg device open failed: %s\n",
				  strerror(ret_errno));
			/* Not all of these map to fi_errno values */
			ret = -ret_errno;
			goto err_exit;
		}

		memset(&get_user_delta_args, 0, sizeof(get_user_delta_args));
		if (ioctl(kdreg_fd, KDREG_IOC_GET_USER_DELTA,
			  &get_user_delta_args) < 0) {
			ret_errno = errno;
			GNIX_WARN(FI_LOG_MR,
				  "kdreg get_user_delta failed: %s\n",
				  strerror(ret_errno));
			close(kdreg_fd);
			/* Not all of these map to fi_errno values */
			ret = -ret_errno;
			goto err_exit;
		}

		if (get_user_delta_args.user_delta == NULL) {
			GNIX_WARN(FI_LOG_MR, "kdreg get_user_delta is NULL\n");
			ret = -FI_ENODATA;
			goto err_exit;
		}

		global_mr_not.fd = kdreg_fd;
		global_mr_not.cntr = (kdreg_user_delta_t *)
				get_user_delta_args.user_delta;
	}

	global_mr_not.ref_cnt++;
	*mrn = &global_mr_not;

err_exit:
	ofi_spin_unlock(&global_mr_not.lock);

	return ret;
}

int
_gnix_notifier_close(struct gnix_mr_notifier *mrn)
{
	int ret = FI_SUCCESS;
	int ret_errno;

	ofi_spin_lock(&mrn->lock);

	ret = notifier_verify_stuff(mrn);
	if (ret != FI_SUCCESS) {
		GNIX_WARN(FI_LOG_MR, "Invalid MR notifier\n");
		goto err_exit;
	}

	assert(mrn->ref_cnt > 0);
	if (--mrn->ref_cnt) {
		goto err_exit;
	}

	if (close(mrn->fd) != 0) {
		ret_errno = errno;
		GNIX_WARN(FI_LOG_MR, "error closing kdreg device: %s\n",
			  strerror(ret_errno));
		/* Not all of these map to fi_errno values */
		ret = -ret_errno;
		goto err_exit;
	}

	mrn->fd = -1;
	mrn->cntr = NULL;
err_exit:
	ofi_spin_unlock(&mrn->lock);

	return ret;
}

static inline int
kdreg_write(struct gnix_mr_notifier *mrn, void *buf, size_t len)
{
	int ret;

	ret = write(mrn->fd, buf, len);
	if ((ret < 0) || (ret != len)) {
		// Not all of these map to fi_errno values
		ret = -errno;
		GNIX_WARN(FI_LOG_MR, "kdreg_write failed: %s\n",
			  strerror(errno));
		return ret;
	}

	return FI_SUCCESS;
}

int
_gnix_notifier_monitor(struct gnix_mr_notifier *mrn,
		    void *addr, uint64_t len, uint64_t cookie)
{
	int ret;
	struct registration_monitor rm;

	ofi_spin_lock(&mrn->lock);

	ret = notifier_verify_stuff(mrn);
	if (ret != FI_SUCCESS) {
		GNIX_WARN(FI_LOG_MR, "Invalid MR notifier\n");
		goto err_exit;
	}

	if (ret == 0) {
		GNIX_DEBUG(FI_LOG_MR, "monitoring %p (len=%lu) cookie=%lu\n",
			   addr, len, cookie);

		memset(&rm, 0, sizeof(rm));
		rm.type = REGISTRATION_MONITOR;
		rm.u.mon.addr = (uint64_t) addr;
		rm.u.mon.len = len;
		rm.u.mon.user_cookie = cookie;

		ret = kdreg_write(mrn, &rm, sizeof(rm));
	}

err_exit:
	ofi_spin_unlock(&mrn->lock);

	return ret;
}

int
_gnix_notifier_unmonitor(struct gnix_mr_notifier *mrn, uint64_t cookie)
{
	int ret;
	struct registration_monitor rm;

	ofi_spin_lock(&mrn->lock);

	ret = notifier_verify_stuff(mrn);
	if (ret != FI_SUCCESS) {
		GNIX_WARN(FI_LOG_MR, "Invalid MR notifier\n");
		goto err_exit;
	}

	GNIX_DEBUG(FI_LOG_MR, "unmonitoring cookie=%lu\n", cookie);

	memset(&rm, 0, sizeof(rm));

	rm.type = REGISTRATION_UNMONITOR;
	rm.u.unmon.user_cookie = cookie;

	ret = kdreg_write(mrn, &rm, sizeof(rm));

err_exit:
	ofi_spin_unlock(&mrn->lock);

	return ret;
}

int
_gnix_notifier_get_event(struct gnix_mr_notifier *mrn, void* buf, size_t len)
{
	int ret, ret_errno;

	if ((mrn == NULL) || (buf == NULL) || (len <= 0)) {
		GNIX_WARN(FI_LOG_MR,
			  "Invalid argument to _gnix_notifier_get_event\n");
		return -FI_EINVAL;
	}

	ofi_spin_lock(&mrn->lock);

	if (*(mrn->cntr) > 0) {
		GNIX_DEBUG(FI_LOG_MR, "reading kdreg event\n");
		ret = read(mrn->fd, buf, len);
		if (ret < 0) {
			ret_errno = errno;
			if (ret_errno != EAGAIN) {
				GNIX_WARN(FI_LOG_MR,
					  "kdreg event read failed: %s\n",
					  strerror(ret_errno));
			}
			/* Not all of these map to fi_errno values */
			ret = -ret_errno;
		}
	} else {
		GNIX_DEBUG(FI_LOG_MR, "nothing to read from kdreg :(\n");
		ret = -FI_EAGAIN;
	}

	ofi_spin_unlock(&mrn->lock);

	return ret;
}

#endif /* HAVE_KDREG */
