/*
 * Copyright (c) 2017 Cray Inc. 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 "rdma/fabric.h"
#include "rdma/fi_errno.h"
#include "fi_ext_gni.h"

#include "gnix_auth_key.h"
#include "gnix_hashtable.h"
#include "gnix_shmem.h"
#include "gnix_bitmap.h"
#include "gnix.h"

#define GNIX_AUTH_KEY_HASHSEED 0xdeadbeef

#define GAI_VERSION(major, minor) (((major) << 16) + (minor))
typedef enum gnix_global_auth_info_version {
	GNIX_GAI_VERSION_1 = GAI_VERSION(1, 0), /* initial version */
} gnix_global_auth_info_version_t;

#define MAX_VMDH_TAGS 4
#define MAX_VMDH_REGS 4096
#define BITMAP_ARR_SIZE(nbits) \
	(GNIX_BITMAP_BLOCKS(nbits) * sizeof(gnix_bitmap_block_t))

#define GNIX_DEFAULT_AK_DIR "/tmp"
#define GNIX_DEFAULT_AK_FILENAME "gnix_vmdh_info"
#define GNIX_DEFAULT_AK_PATH \
	GNIX_DEFAULT_AK_DIR "/" GNIX_DEFAULT_AK_FILENAME
#define GNIX_AK_PATH_LEN 256

static char *gnix_default_ak_path = GNIX_DEFAULT_AK_PATH;

uint8_t* gnix_default_auth_key = NULL;

struct gnix_global_ptag_info {
	gnix_bitmap_t prov;
	gnix_bitmap_t user;
	uint8_t arr[BITMAP_ARR_SIZE(MAX_VMDH_REGS)];
};

struct gnix_global_vmdh_info {
	uint32_t version;
	uint32_t size;
	uint32_t table_size;
	ofi_spin_t lock;
	int ptag_index[MAX_VMDH_TAGS];
	struct gnix_global_ptag_info info[MAX_VMDH_TAGS];
};

/* Global data storage for authorization key information */
/* gnix_vmdh_info is the shared memory synchronization area for ptag info */
static struct gnix_shared_memory __gnix_shmem_region;
static struct gnix_global_vmdh_info *__gnix_vmdh_info;
gnix_hashtable_t __gnix_auth_key_ht;

static int __gnix_global_vmdh_info_init(const char *path,
	uint32_t size, void *buffer)
{
	struct gnix_global_vmdh_info *info =
		(struct gnix_global_vmdh_info *) buffer;
	int i;

	memset(info, 0, sizeof(struct gnix_global_vmdh_info));

	info->version = GNIX_GAI_VERSION_1;
	info->size = size;
	info->table_size = _gnix_bitmap_get_buffer_size(MAX_VMDH_REGS);
	for (i = 0; i < MAX_VMDH_TAGS; i++)
		info->ptag_index[i] = -1;
	ofi_spin_init(&info->lock);

	return 0;
}

static int _gnix_open_vmdh_info_file(const char *path)
{
	int ret;

	if (!__gnix_vmdh_info) {
		ret = _gnix_shmem_create(path,
			sizeof(struct gnix_global_vmdh_info),
			__gnix_global_vmdh_info_init,
			&__gnix_shmem_region);
		if (ret)
			return ret;

		__gnix_vmdh_info = (struct gnix_global_vmdh_info *)
			__gnix_shmem_region.addr;
	}

	if (__gnix_vmdh_info->version != GNIX_GAI_VERSION_1)
		GNIX_FATAL(FI_LOG_FABRIC,
			"failed to find compatible version of "
			"vmdh information file, expected=%x actual=%x\n",
			GNIX_GAI_VERSION_1, __gnix_vmdh_info->version);

	return 0;
}
int _gnix_get_next_reserved_key(struct gnix_auth_key *info)
{
	int reserved_key;
	int offset = info->attr.user_key_limit;
	int retry_limit = 10; /* randomly picked */
	int ret;

	if (!info) {
		GNIX_WARN(FI_LOG_MR, "bad authorization key, key=%p\n",
			info);
		return -FI_EINVAL;
	}

	do {
		reserved_key = _gnix_find_first_zero_bit(info->prov);
		if (reserved_key >= 0) {
			ret = _gnix_test_and_set_bit(info->prov, reserved_key);
			if (ret)
				reserved_key = -FI_EAGAIN;
		}
		retry_limit--;
	} while (reserved_key < 0 && retry_limit > 0);

	ret = (reserved_key < 0) ? reserved_key : (offset + reserved_key);

	GNIX_INFO(FI_LOG_DOMAIN, "returning key=%d offset=%d\n", ret, offset);

	return ret;
}

int _gnix_release_reserved_key(struct gnix_auth_key *info, int reserved_key)
{
	int offset = info->attr.user_key_limit;
	int ret;

	if (!info || reserved_key < 0) {
		GNIX_WARN(FI_LOG_MR, "bad authorization key or reserved key,"
			" auth_key=%p requested_key=%d\n",
			info, reserved_key);
		return -FI_EINVAL;
	}

	ret = _gnix_test_and_clear_bit(info->prov, reserved_key - offset);
	assert(ret == 1);

	return (ret == 1) ? FI_SUCCESS : -FI_EBUSY;
}

static inline int __gnix_auth_key_enable_vmdh(struct gnix_auth_key *info)
{
	int i, ret;
	void *buffer;

	ofi_spin_lock(&__gnix_vmdh_info->lock);
	/* Find ptag in node-local info structure */
	for (i = 0; i < MAX_VMDH_TAGS; i++)
		if (__gnix_vmdh_info->ptag_index[i] == info->ptag)
			break;

	if (i == MAX_VMDH_TAGS) { /* didn't find it */
		/* find first empty region */
		for (i = 0; i < MAX_VMDH_TAGS; i++)
			if (__gnix_vmdh_info->ptag_index[i] == -1)
				break;

		/* if no space ... */
		if (i == MAX_VMDH_TAGS) {
			ofi_spin_unlock(&__gnix_vmdh_info->lock);
			GNIX_WARN(FI_LOG_FABRIC,
				"application is attempting to use too many keys "
				"with scalable memory registration, "
				"ret=-FI_ENOSPC\n");
			return -FI_ENOSPC;
		}

		/* set index entry to ptag ID */
		__gnix_vmdh_info->ptag_index[i] = info->ptag;

		/* setup provider key space */
		buffer = (void *) __gnix_vmdh_info->info[i].arr;
		ret = _gnix_alloc_bitmap(&__gnix_vmdh_info->info[i].prov,
			info->attr.prov_key_limit, buffer);
		if (ret) {
			ofi_spin_unlock(&__gnix_vmdh_info->lock);
			GNIX_WARN(FI_LOG_FABRIC,
				"failed to allocate bitmap on mmap backed page, ret=%d\n",
				ret);
			return ret;
		}

		/* advance buffer and setup user key space */
		buffer = (void *) ((uint64_t) (buffer) +
			 _gnix_bitmap_get_buffer_size(info->attr.prov_key_limit));

		ret = _gnix_alloc_bitmap(&__gnix_vmdh_info->info[i].user,
			info->attr.user_key_limit, buffer);
		if (ret) {
			ofi_spin_unlock(&__gnix_vmdh_info->lock);
			GNIX_WARN(FI_LOG_FABRIC,
				"failed to allocate bitmap on mmap backed page, ret=%d\n",
				ret);
			return ret;
		}

		GNIX_INFO(FI_LOG_FABRIC,
			"set resource limits: pkey=%08x ptag=%d "
			"reserved=%d registration_limit=%d "
			"reserved_keys=%d-%d\n",
			info->cookie,
			info->ptag,
			info->attr.prov_key_limit,
			info->attr.user_key_limit,
			info->attr.user_key_limit,
			(info->attr.prov_key_limit +
			info->attr.user_key_limit - 1));
	}
	info->prov = &__gnix_vmdh_info->info[i].prov;
	info->user = &__gnix_vmdh_info->info[i].user;
	ofi_spin_unlock(&__gnix_vmdh_info->lock);

	return FI_SUCCESS;
}

int _gnix_auth_key_enable(struct gnix_auth_key *info)
{
	int ret = -FI_EBUSY;
	uint32_t pes_on_node;
	int logical_rank;

	if (!info) {
		GNIX_WARN(FI_LOG_MR, "bad authorization key, key=%p\n",
			info);
		return -FI_EINVAL;
	}

	ofi_spin_lock(&info->lock);
	if (!info->enabled) {
		info->enabled = 1;

		if (info->using_vmdh) {
			if (!__gnix_vmdh_info) {
				char *dir = getenv("TMPDIR");
				char *filename = getenv("GNIX_AK_FILENAME");
				char path[GNIX_AK_PATH_LEN];
				int sz, i;

				if (dir || filename) {
					GNIX_DEBUG(FI_LOG_FABRIC,
						"using non-default directory or file name, "
						"errors may occur\n");
					if (!dir)
						sz = snprintf(path, GNIX_AK_PATH_LEN,
							"%s/", GNIX_DEFAULT_AK_DIR);
					else
						sz = snprintf(path, GNIX_AK_PATH_LEN,
							"%s/", dir);

					if (sz < 0)
						return -FI_EINVAL;

					i = sz;
					if (!filename)
						sz = snprintf(&path[i], GNIX_AK_PATH_LEN - i,
							"%s", GNIX_DEFAULT_AK_FILENAME);
					else
						sz = snprintf(&path[i], GNIX_AK_PATH_LEN - i,
							"%s", filename);

					if (sz < 0)
						return -FI_EINVAL;
					sz += i;
				} else {
					sz = snprintf(path, GNIX_AK_PATH_LEN, "%s",
						gnix_default_ak_path);
				}

				path[(sz == GNIX_AK_PATH_LEN) ?
					GNIX_AK_PATH_LEN : sz + 1] = '\0';
				if (sz == GNIX_AK_PATH_LEN) {
					GNIX_WARN(FI_LOG_FABRIC,
						"file path maximum length exceeded, "
						"truncating name to 256 characters path=%s\n",
						path);
				}

				GNIX_INFO(FI_LOG_FABRIC,
					"opening auth key file at %s\n", path);

				ret = _gnix_open_vmdh_info_file(path);
				if (ret) {
					info->enabled = 0;
					ofi_spin_unlock(&info->lock);
					return ret;
				}
			}

			ret = __gnix_auth_key_enable_vmdh(info);
			if (ret) {
				info->enabled = 0;
				ofi_spin_unlock(&info->lock);
				return ret;
			}

			ret = _gnix_pes_on_node(&pes_on_node);
			if (ret)
				GNIX_FATAL(FI_LOG_DOMAIN,
					"failed to get count of pes on node, rc=%d\n", ret);

			ret = _gnix_pe_node_rank(&logical_rank);
			if (ret)
				GNIX_FATAL(FI_LOG_DOMAIN,
					"failed to get logical node rank, rc=%d\n", ret);

			info->key_partition_size = info->attr.user_key_limit / pes_on_node;
			info->key_offset = logical_rank * info->key_partition_size;
		}
		GNIX_INFO(FI_LOG_DOMAIN,
			"pkey=%08x ptag=%d key_partition_size=%d key_offset=%d enabled\n",
			info->cookie, info->ptag, info->key_partition_size, info->key_offset);
		ret = FI_SUCCESS;
	}
	ofi_spin_unlock(&info->lock);

	if (ret == -FI_EBUSY) {
		GNIX_DEBUG(FI_LOG_MR, "authorization key already enabled, "
			"auth_key=%p\n", info);
	}

	return ret;
}

struct gnix_auth_key *_gnix_auth_key_alloc()
{
	struct gnix_auth_key *auth_key = NULL;

	auth_key = calloc(1, sizeof(*auth_key));
	if (auth_key) {
		ofi_spin_init(&auth_key->lock);
	} else {
		GNIX_WARN(FI_LOG_MR, "failed to allocate memory for "
			"authorization key\n");
	}

	return auth_key;
}

int _gnix_auth_key_insert(
		uint8_t *auth_key,
		size_t auth_key_size,
		struct gnix_auth_key *to_insert)
{
	int ret;
	gnix_ht_key_t key;
	struct fi_gni_auth_key *gni_auth_key =
		(struct fi_gni_auth_key *) auth_key;

	if (!to_insert) {
		GNIX_WARN(FI_LOG_MR, "bad parameters, to_insert=%p\n",
			to_insert);
		return -FI_EINVAL;
	}

	if (!auth_key) {
		GNIX_INFO(FI_LOG_FABRIC, "auth key is null\n");
		return -FI_EINVAL;
	}

	switch (gni_auth_key->type) {
	case GNIX_AKT_RAW:
		key = (gnix_ht_key_t) gni_auth_key->raw.protection_key;
		break;
	default:
		GNIX_INFO(FI_LOG_FABRIC, "unrecognized auth key "
			"type, type=%d\n",
			gni_auth_key->type);
		return -FI_EINVAL;
	}

	ret = _gnix_ht_insert(&__gnix_auth_key_ht, key, to_insert);
	if (ret) {
		GNIX_WARN(FI_LOG_MR, "failed to insert entry, ret=%d\n",
			ret);
	}

	return ret;
}

int _gnix_auth_key_free(struct gnix_auth_key *key)
{
	if (!key) {
		GNIX_WARN(FI_LOG_MR, "bad parameters, key=%p\n", key);
		return -FI_EINVAL;
	}

	ofi_spin_destroy(&key->lock);

	key->enabled = 0;

	free(key);

	return FI_SUCCESS;
}

struct gnix_auth_key *
_gnix_auth_key_lookup(uint8_t *auth_key, size_t auth_key_size)
{
	gnix_ht_key_t key;
	struct gnix_auth_key *ptr = NULL;
	struct fi_gni_auth_key *gni_auth_key = NULL;

	if (auth_key == NULL || auth_key_size == 0) {
		auth_key = gnix_default_auth_key;
	}

	gni_auth_key = (struct fi_gni_auth_key *) auth_key;
	switch (gni_auth_key->type) {
	case GNIX_AKT_RAW:
		key = (gnix_ht_key_t) gni_auth_key->raw.protection_key;
		break;
	default:
		GNIX_INFO(FI_LOG_FABRIC, "unrecognized auth key type, "
			"type=%d\n", gni_auth_key->type);
		return NULL;
	}

	ptr = (struct gnix_auth_key *) _gnix_ht_lookup(
		&__gnix_auth_key_ht, key);

	return ptr;
}

int _gnix_auth_key_subsys_init(void)
{
	int ret = FI_SUCCESS;

	gnix_hashtable_attr_t attr = {
			.ht_initial_size     = 8,
			.ht_maximum_size     = 256,
			.ht_increase_step    = 2,
			.ht_increase_type    = GNIX_HT_INCREASE_MULT,
			.ht_collision_thresh = 400,
			.ht_hash_seed        = 0xcafed00d,
			.ht_internal_locking = 1,
			.destructor          = NULL
	};

	ret = _gnix_ht_init(&__gnix_auth_key_ht, &attr);
	assert(ret == FI_SUCCESS);

	struct fi_gni_auth_key *gni_auth_key = calloc(1, sizeof(*gni_auth_key));
	gni_auth_key->type = GNIX_AKT_RAW;
	gni_auth_key->raw.protection_key = 0;
	gnix_default_auth_key = (uint8_t *) gni_auth_key;

	return ret;
}

int _gnix_auth_key_subsys_fini(void)
{
	free(gnix_default_auth_key);

	return FI_SUCCESS;
}

struct gnix_auth_key *_gnix_auth_key_create(
		uint8_t *auth_key,
		size_t auth_key_size)
{
	struct gnix_auth_key *to_insert;
	struct fi_gni_auth_key *gni_auth_key;
	int ret;
	gni_return_t grc;
	uint8_t ptag;
	uint32_t cookie;

	if (auth_key == NULL || auth_key_size == 0) {
		auth_key = gnix_default_auth_key;
	}

	gni_auth_key = (struct fi_gni_auth_key *) auth_key;
	if (auth_key == gnix_default_auth_key) {
		gnixu_get_rdma_credentials(NULL, &ptag, &cookie);
		gni_auth_key->raw.protection_key = cookie;
	} else {
		switch (gni_auth_key->type) {
		case GNIX_AKT_RAW:
			cookie = gni_auth_key->raw.protection_key;
			break;
		default:
			GNIX_WARN(FI_LOG_FABRIC,
				"unrecognized auth key type, type=%d\n",
				gni_auth_key->type);
			return NULL;
		}

		grc = GNI_GetPtag(0, cookie, &ptag);
		if (grc) {
			GNIX_WARN(FI_LOG_FABRIC,
				"could not retrieve ptag, "
				"cookie=%d ret=%d\n", cookie, grc);
			return NULL;
		}
	}

	to_insert = _gnix_auth_key_alloc();
	if (!to_insert) {
		GNIX_WARN(FI_LOG_MR, "failed to allocate memory for "
			"auth key\n");
		return NULL;
	}

	to_insert->attr.prov_key_limit = gnix_default_prov_registration_limit;
	to_insert->attr.user_key_limit = gnix_default_user_registration_limit;
	to_insert->ptag = ptag;
	to_insert->cookie = cookie;

	ret = _gnix_auth_key_insert(auth_key, auth_key_size, to_insert);
	if (ret) {
		GNIX_INFO(FI_LOG_MR, "failed to insert authorization key, "
			"key=%p len=%d to_insert=%p ret=%d\n",
			auth_key, auth_key_size, to_insert, ret);
		_gnix_auth_key_free(to_insert);
		to_insert = NULL;
	}

	return to_insert;
}
