/* SPDX-License-Identifier: BSD-3-Clause
 * Copyright(c) 2018 Netronome Systems, Inc.
 * All rights reserved.
 */

#include <errno.h>

#include <malloc.h>
#include <time.h>
#include <sched.h>

#include "nfp_cpp.h"
#include "nfp6000/nfp6000.h"

#define MUTEX_LOCKED(interface)  ((((uint32_t)(interface)) << 16) | 0x000f)
#define MUTEX_UNLOCK(interface)  (0                               | 0x0000)

#define MUTEX_IS_LOCKED(value)   (((value) & 0xffff) == 0x000f)
#define MUTEX_IS_UNLOCKED(value) (((value) & 0xffff) == 0x0000)
#define MUTEX_INTERFACE(value)   (((value) >> 16) & 0xffff)

/*
 * If you need more than 65536 recursive locks, please
 * rethink your code.
 */
#define MUTEX_DEPTH_MAX         0xffff

struct nfp_cpp_mutex {
	struct nfp_cpp *cpp;
	uint8_t target;
	uint16_t depth;
	unsigned long long address;
	uint32_t key;
	unsigned int usage;
	struct nfp_cpp_mutex *prev, *next;
};

static int
_nfp_cpp_mutex_validate(uint32_t model, int *target, unsigned long long address)
{
	/* Address must be 64-bit aligned */
	if (address & 7)
		return NFP_ERRNO(EINVAL);

	if (NFP_CPP_MODEL_IS_6000(model)) {
		if (*target != NFP_CPP_TARGET_MU)
			return NFP_ERRNO(EINVAL);
	} else {
		return NFP_ERRNO(EINVAL);
	}

	return 0;
}

/*
 * Initialize a mutex location
 *
 * The CPP target:address must point to a 64-bit aligned location, and
 * will initialize 64 bits of data at the location.
 *
 * This creates the initial mutex state, as locked by this
 * nfp_cpp_interface().
 *
 * This function should only be called when setting up
 * the initial lock state upon boot-up of the system.
 *
 * @param mutex     NFP CPP Mutex handle
 * @param target    NFP CPP target ID (ie NFP_CPP_TARGET_CLS or
 *		    NFP_CPP_TARGET_MU)
 * @param address   Offset into the address space of the NFP CPP target ID
 * @param key       Unique 32-bit value for this mutex
 *
 * @return 0 on success, or -1 on failure (and set errno accordingly).
 */
int
nfp_cpp_mutex_init(struct nfp_cpp *cpp, int target, unsigned long long address,
		   uint32_t key)
{
	uint32_t model = nfp_cpp_model(cpp);
	uint32_t muw = NFP_CPP_ID(target, 4, 0);	/* atomic_write */
	int err;

	err = _nfp_cpp_mutex_validate(model, &target, address);
	if (err < 0)
		return err;

	err = nfp_cpp_writel(cpp, muw, address + 4, key);
	if (err < 0)
		return err;

	err =
	    nfp_cpp_writel(cpp, muw, address + 0,
			   MUTEX_LOCKED(nfp_cpp_interface(cpp)));
	if (err < 0)
		return err;

	return 0;
}

/*
 * Create a mutex handle from an address controlled by a MU Atomic engine
 *
 * The CPP target:address must point to a 64-bit aligned location, and
 * reserve 64 bits of data at the location for use by the handle.
 *
 * Only target/address pairs that point to entities that support the
 * MU Atomic Engine are supported.
 *
 * @param cpp       NFP CPP handle
 * @param target    NFP CPP target ID (ie NFP_CPP_TARGET_CLS or
 *		    NFP_CPP_TARGET_MU)
 * @param address   Offset into the address space of the NFP CPP target ID
 * @param key       32-bit unique key (must match the key at this location)
 *
 * @return      A non-NULL struct nfp_cpp_mutex * on success, NULL on failure.
 */
struct nfp_cpp_mutex *
nfp_cpp_mutex_alloc(struct nfp_cpp *cpp, int target,
		     unsigned long long address, uint32_t key)
{
	uint32_t model = nfp_cpp_model(cpp);
	struct nfp_cpp_mutex *mutex;
	uint32_t mur = NFP_CPP_ID(target, 3, 0);	/* atomic_read */
	int err;
	uint32_t tmp;

	/* Look for cached mutex */
	for (mutex = cpp->mutex_cache; mutex; mutex = mutex->next) {
		if (mutex->target == target && mutex->address == address)
			break;
	}

	if (mutex) {
		if (mutex->key == key) {
			mutex->usage++;
			return mutex;
		}

		/* If the key doesn't match... */
		return NFP_ERRPTR(EEXIST);
	}

	err = _nfp_cpp_mutex_validate(model, &target, address);
	if (err < 0)
		return NULL;

	err = nfp_cpp_readl(cpp, mur, address + 4, &tmp);
	if (err < 0)
		return NULL;

	if (tmp != key)
		return NFP_ERRPTR(EEXIST);

	mutex = calloc(sizeof(*mutex), 1);
	if (!mutex)
		return NFP_ERRPTR(ENOMEM);

	mutex->cpp = cpp;
	mutex->target = target;
	mutex->address = address;
	mutex->key = key;
	mutex->depth = 0;
	mutex->usage = 1;

	/* Add mutex to the cache */
	if (cpp->mutex_cache) {
		cpp->mutex_cache->prev = mutex;
		mutex->next = cpp->mutex_cache;
		cpp->mutex_cache = mutex;
	} else {
		cpp->mutex_cache = mutex;
	}

	return mutex;
}

struct nfp_cpp *
nfp_cpp_mutex_cpp(struct nfp_cpp_mutex *mutex)
{
	return mutex->cpp;
}

uint32_t
nfp_cpp_mutex_key(struct nfp_cpp_mutex *mutex)
{
	return mutex->key;
}

uint16_t
nfp_cpp_mutex_owner(struct nfp_cpp_mutex *mutex)
{
	uint32_t mur = NFP_CPP_ID(mutex->target, 3, 0);	/* atomic_read */
	uint32_t value, key;
	int err;

	err = nfp_cpp_readl(mutex->cpp, mur, mutex->address, &value);
	if (err < 0)
		return err;

	err = nfp_cpp_readl(mutex->cpp, mur, mutex->address + 4, &key);
	if (err < 0)
		return err;

	if (key != mutex->key)
		return NFP_ERRNO(EPERM);

	if (!MUTEX_IS_LOCKED(value))
		return 0;

	return MUTEX_INTERFACE(value);
}

int
nfp_cpp_mutex_target(struct nfp_cpp_mutex *mutex)
{
	return mutex->target;
}

uint64_t
nfp_cpp_mutex_address(struct nfp_cpp_mutex *mutex)
{
	return mutex->address;
}

/*
 * Free a mutex handle - does not alter the lock state
 *
 * @param mutex     NFP CPP Mutex handle
 */
void
nfp_cpp_mutex_free(struct nfp_cpp_mutex *mutex)
{
	mutex->usage--;
	if (mutex->usage > 0)
		return;

	/* Remove mutex from the cache */
	if (mutex->next)
		mutex->next->prev = mutex->prev;
	if (mutex->prev)
		mutex->prev->next = mutex->next;

	/* If mutex->cpp == NULL, something broke */
	if (mutex->cpp && mutex == mutex->cpp->mutex_cache)
		mutex->cpp->mutex_cache = mutex->next;

	free(mutex);
}

/*
 * Lock a mutex handle, using the NFP MU Atomic Engine
 *
 * @param mutex     NFP CPP Mutex handle
 *
 * @return 0 on success, or -1 on failure (and set errno accordingly).
 */
int
nfp_cpp_mutex_lock(struct nfp_cpp_mutex *mutex)
{
	int err;
	time_t warn_at = time(NULL) + 15;

	while ((err = nfp_cpp_mutex_trylock(mutex)) != 0) {
		/* If errno != EBUSY, then the lock was damaged */
		if (err < 0 && errno != EBUSY)
			return err;
		if (time(NULL) >= warn_at) {
			printf("Warning: waiting for NFP mutex\n");
			printf("\tusage:%u\n", mutex->usage);
			printf("\tdepth:%hd]\n", mutex->depth);
			printf("\ttarget:%d\n", mutex->target);
			printf("\taddr:%llx\n", mutex->address);
			printf("\tkey:%08x]\n", mutex->key);
			warn_at = time(NULL) + 60;
		}
		sched_yield();
	}
	return 0;
}

/*
 * Unlock a mutex handle, using the NFP MU Atomic Engine
 *
 * @param mutex     NFP CPP Mutex handle
 *
 * @return 0 on success, or -1 on failure (and set errno accordingly).
 */
int
nfp_cpp_mutex_unlock(struct nfp_cpp_mutex *mutex)
{
	uint32_t muw = NFP_CPP_ID(mutex->target, 4, 0);	/* atomic_write */
	uint32_t mur = NFP_CPP_ID(mutex->target, 3, 0);	/* atomic_read */
	struct nfp_cpp *cpp = mutex->cpp;
	uint32_t key, value;
	uint16_t interface = nfp_cpp_interface(cpp);
	int err;

	if (mutex->depth > 1) {
		mutex->depth--;
		return 0;
	}

	err = nfp_cpp_readl(mutex->cpp, mur, mutex->address, &value);
	if (err < 0)
		goto exit;

	err = nfp_cpp_readl(mutex->cpp, mur, mutex->address + 4, &key);
	if (err < 0)
		goto exit;

	if (key != mutex->key) {
		err = NFP_ERRNO(EPERM);
		goto exit;
	}

	if (value != MUTEX_LOCKED(interface)) {
		err = NFP_ERRNO(EACCES);
		goto exit;
	}

	err = nfp_cpp_writel(cpp, muw, mutex->address, MUTEX_UNLOCK(interface));
	if (err < 0)
		goto exit;

	mutex->depth = 0;

exit:
	return err;
}

/*
 * Attempt to lock a mutex handle, using the NFP MU Atomic Engine
 *
 * Valid lock states:
 *
 *      0x....0000      - Unlocked
 *      0x....000f      - Locked
 *
 * @param mutex     NFP CPP Mutex handle
 * @return      0 if the lock succeeded, -1 on failure (and errno set
 *		appropriately).
 */
int
nfp_cpp_mutex_trylock(struct nfp_cpp_mutex *mutex)
{
	uint32_t mur = NFP_CPP_ID(mutex->target, 3, 0);	/* atomic_read */
	uint32_t muw = NFP_CPP_ID(mutex->target, 4, 0);	/* atomic_write */
	uint32_t mus = NFP_CPP_ID(mutex->target, 5, 3);	/* test_set_imm */
	uint32_t key, value, tmp;
	struct nfp_cpp *cpp = mutex->cpp;
	int err;

	if (mutex->depth > 0) {
		if (mutex->depth == MUTEX_DEPTH_MAX)
			return NFP_ERRNO(E2BIG);

		mutex->depth++;
		return 0;
	}

	/* Verify that the lock marker is not damaged */
	err = nfp_cpp_readl(cpp, mur, mutex->address + 4, &key);
	if (err < 0)
		goto exit;

	if (key != mutex->key) {
		err = NFP_ERRNO(EPERM);
		goto exit;
	}

	/*
	 * Compare against the unlocked state, and if true,
	 * write the interface id into the top 16 bits, and
	 * mark as locked.
	 */
	value = MUTEX_LOCKED(nfp_cpp_interface(cpp));

	/*
	 * We use test_set_imm here, as it implies a read
	 * of the current state, and sets the bits in the
	 * bytemask of the command to 1s. Since the mutex
	 * is guaranteed to be 64-bit aligned, the bytemask
	 * of this 32-bit command is ensured to be 8'b00001111,
	 * which implies that the lower 4 bits will be set to
	 * ones regardless of the initial state.
	 *
	 * Since this is a 'Readback' operation, with no Pull
	 * data, we can treat this as a normal Push (read)
	 * atomic, which returns the original value.
	 */
	err = nfp_cpp_readl(cpp, mus, mutex->address, &tmp);
	if (err < 0)
		goto exit;

	/* Was it unlocked? */
	if (MUTEX_IS_UNLOCKED(tmp)) {
		/*
		 * The read value can only be 0x....0000 in the unlocked state.
		 * If there was another contending for this lock, then
		 * the lock state would be 0x....000f
		 *
		 * Write our owner ID into the lock
		 * While not strictly necessary, this helps with
		 * debug and bookkeeping.
		 */
		err = nfp_cpp_writel(cpp, muw, mutex->address, value);
		if (err < 0)
			goto exit;

		mutex->depth = 1;
		goto exit;
	}

	/* Already locked by us? Success! */
	if (tmp == value) {
		mutex->depth = 1;
		goto exit;
	}

	err = NFP_ERRNO(MUTEX_IS_LOCKED(tmp) ? EBUSY : EINVAL);

exit:
	return err;
}
