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

#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>

#include <rte_byteorder.h>
#include <rte_ethdev_pci.h>

#include "nfp_cpp.h"
#include "nfp_target.h"
#include "nfp6000/nfp6000.h"
#include "nfp6000/nfp_xpb.h"
#include "nfp_nffw.h"

#define NFP_PL_DEVICE_ID                        0x00000004
#define NFP_PL_DEVICE_ID_MASK                   0xff

#define NFP6000_ARM_GCSR_SOFTMODEL0             0x00400144

void
nfp_cpp_priv_set(struct nfp_cpp *cpp, void *priv)
{
	cpp->priv = priv;
}

void *
nfp_cpp_priv(struct nfp_cpp *cpp)
{
	return cpp->priv;
}

void
nfp_cpp_model_set(struct nfp_cpp *cpp, uint32_t model)
{
	cpp->model = model;
}

uint32_t
nfp_cpp_model(struct nfp_cpp *cpp)
{
	if (!cpp)
		return NFP_CPP_MODEL_INVALID;

	if (cpp->model == 0)
		cpp->model = __nfp_cpp_model_autodetect(cpp);

	return cpp->model;
}

void
nfp_cpp_interface_set(struct nfp_cpp *cpp, uint32_t interface)
{
	cpp->interface = interface;
}

int
nfp_cpp_serial(struct nfp_cpp *cpp, const uint8_t **serial)
{
	*serial = cpp->serial;
	return cpp->serial_len;
}

int
nfp_cpp_serial_set(struct nfp_cpp *cpp, const uint8_t *serial,
		   size_t serial_len)
{
	if (cpp->serial_len)
		free(cpp->serial);

	cpp->serial = malloc(serial_len);
	if (!cpp->serial)
		return -1;

	memcpy(cpp->serial, serial, serial_len);
	cpp->serial_len = serial_len;

	return 0;
}

uint16_t
nfp_cpp_interface(struct nfp_cpp *cpp)
{
	if (!cpp)
		return NFP_CPP_INTERFACE(NFP_CPP_INTERFACE_TYPE_INVALID, 0, 0);

	return cpp->interface;
}

void *
nfp_cpp_area_priv(struct nfp_cpp_area *cpp_area)
{
	return &cpp_area[1];
}

struct nfp_cpp *
nfp_cpp_area_cpp(struct nfp_cpp_area *cpp_area)
{
	return cpp_area->cpp;
}

const char *
nfp_cpp_area_name(struct nfp_cpp_area *cpp_area)
{
	return cpp_area->name;
}

/*
 * nfp_cpp_area_alloc - allocate a new CPP area
 * @cpp:    CPP handle
 * @dest:   CPP id
 * @address:    start address on CPP target
 * @size:   size of area in bytes
 *
 * Allocate and initialize a CPP area structure.  The area must later
 * be locked down with an 'acquire' before it can be safely accessed.
 *
 * NOTE: @address and @size must be 32-bit aligned values.
 */
struct nfp_cpp_area *
nfp_cpp_area_alloc_with_name(struct nfp_cpp *cpp, uint32_t dest,
			      const char *name, unsigned long long address,
			      unsigned long size)
{
	struct nfp_cpp_area *area;
	uint64_t tmp64 = (uint64_t)address;
	int tmp, err;

	if (!cpp)
		return NULL;

	/* CPP bus uses only a 40-bit address */
	if ((address + size) > (1ULL << 40))
		return NFP_ERRPTR(EFAULT);

	/* Remap from cpp_island to cpp_target */
	err = nfp_target_cpp(dest, tmp64, &dest, &tmp64, cpp->imb_cat_table);
	if (err < 0)
		return NULL;

	address = (unsigned long long)tmp64;

	if (!name)
		name = "";

	area = calloc(1, sizeof(*area) + cpp->op->area_priv_size +
		      strlen(name) + 1);
	if (!area)
		return NULL;

	area->cpp = cpp;
	area->name = ((char *)area) + sizeof(*area) + cpp->op->area_priv_size;
	memcpy(area->name, name, strlen(name) + 1);

	/*
	 * Preserve errno around the call to area_init, since most
	 * implementations will blindly call nfp_target_action_width()for both
	 * read or write modes, and that will set errno to EINVAL.
	 */
	tmp = errno;

	err = cpp->op->area_init(area, dest, address, size);
	if (err < 0) {
		free(area);
		return NULL;
	}

	/* Restore errno */
	errno = tmp;

	area->offset = address;
	area->size = size;

	return area;
}

struct nfp_cpp_area *
nfp_cpp_area_alloc(struct nfp_cpp *cpp, uint32_t dest,
		    unsigned long long address, unsigned long size)
{
	return nfp_cpp_area_alloc_with_name(cpp, dest, NULL, address, size);
}

/*
 * nfp_cpp_area_alloc_acquire - allocate a new CPP area and lock it down
 *
 * @cpp:    CPP handle
 * @dest:   CPP id
 * @address:    start address on CPP target
 * @size:   size of area
 *
 * Allocate and initilizae a CPP area structure, and lock it down so
 * that it can be accessed directly.
 *
 * NOTE: @address and @size must be 32-bit aligned values.
 *
 * NOTE: The area must also be 'released' when the structure is freed.
 */
struct nfp_cpp_area *
nfp_cpp_area_alloc_acquire(struct nfp_cpp *cpp, uint32_t destination,
			    unsigned long long address, unsigned long size)
{
	struct nfp_cpp_area *area;

	area = nfp_cpp_area_alloc(cpp, destination, address, size);
	if (!area)
		return NULL;

	if (nfp_cpp_area_acquire(area)) {
		nfp_cpp_area_free(area);
		return NULL;
	}

	return area;
}

/*
 * nfp_cpp_area_free - free up the CPP area
 * area:    CPP area handle
 *
 * Frees up memory resources held by the CPP area.
 */
void
nfp_cpp_area_free(struct nfp_cpp_area *area)
{
	if (area->cpp->op->area_cleanup)
		area->cpp->op->area_cleanup(area);
	free(area);
}

/*
 * nfp_cpp_area_release_free - release CPP area and free it
 * area:    CPP area handle
 *
 * Releases CPP area and frees up memory resources held by the it.
 */
void
nfp_cpp_area_release_free(struct nfp_cpp_area *area)
{
	nfp_cpp_area_release(area);
	nfp_cpp_area_free(area);
}

/*
 * nfp_cpp_area_acquire - lock down a CPP area for access
 * @area:   CPP area handle
 *
 * Locks down the CPP area for a potential long term activity.  Area
 * must always be locked down before being accessed.
 */
int
nfp_cpp_area_acquire(struct nfp_cpp_area *area)
{
	if (area->cpp->op->area_acquire) {
		int err = area->cpp->op->area_acquire(area);

		if (err < 0)
			return -1;
	}

	return 0;
}

/*
 * nfp_cpp_area_release - release a locked down CPP area
 * @area:   CPP area handle
 *
 * Releases a previously locked down CPP area.
 */
void
nfp_cpp_area_release(struct nfp_cpp_area *area)
{
	if (area->cpp->op->area_release)
		area->cpp->op->area_release(area);
}

/*
 * nfp_cpp_area_iomem() - get IOMEM region for CPP area
 *
 * @area:       CPP area handle
 *
 * Returns an iomem pointer for use with readl()/writel() style operations.
 *
 * NOTE: Area must have been locked down with an 'acquire'.
 *
 * Return: pointer to the area, or NULL
 */
void *
nfp_cpp_area_iomem(struct nfp_cpp_area *area)
{
	void *iomem = NULL;

	if (area->cpp->op->area_iomem)
		iomem = area->cpp->op->area_iomem(area);

	return iomem;
}

/*
 * nfp_cpp_area_read - read data from CPP area
 *
 * @area:       CPP area handle
 * @offset:     offset into CPP area
 * @kernel_vaddr:   kernel address to put data into
 * @length:     number of bytes to read
 *
 * Read data from indicated CPP region.
 *
 * NOTE: @offset and @length must be 32-bit aligned values.
 *
 * NOTE: Area must have been locked down with an 'acquire'.
 */
int
nfp_cpp_area_read(struct nfp_cpp_area *area, unsigned long offset,
		  void *kernel_vaddr, size_t length)
{
	if ((offset + length) > area->size)
		return NFP_ERRNO(EFAULT);

	return area->cpp->op->area_read(area, kernel_vaddr, offset, length);
}

/*
 * nfp_cpp_area_write - write data to CPP area
 *
 * @area:       CPP area handle
 * @offset:     offset into CPP area
 * @kernel_vaddr:   kernel address to read data from
 * @length:     number of bytes to write
 *
 * Write data to indicated CPP region.
 *
 * NOTE: @offset and @length must be 32-bit aligned values.
 *
 * NOTE: Area must have been locked down with an 'acquire'.
 */
int
nfp_cpp_area_write(struct nfp_cpp_area *area, unsigned long offset,
		   const void *kernel_vaddr, size_t length)
{
	if ((offset + length) > area->size)
		return NFP_ERRNO(EFAULT);

	return area->cpp->op->area_write(area, kernel_vaddr, offset, length);
}

void *
nfp_cpp_area_mapped(struct nfp_cpp_area *area)
{
	if (area->cpp->op->area_mapped)
		return area->cpp->op->area_mapped(area);
	return NULL;
}

/*
 * nfp_cpp_area_check_range - check if address range fits in CPP area
 *
 * @area:   CPP area handle
 * @offset: offset into CPP area
 * @length: size of address range in bytes
 *
 * Check if address range fits within CPP area.  Return 0 if area fits
 * or -1 on error.
 */
int
nfp_cpp_area_check_range(struct nfp_cpp_area *area, unsigned long long offset,
			 unsigned long length)
{
	if (((offset + length) > area->size))
		return NFP_ERRNO(EFAULT);

	return 0;
}

/*
 * Return the correct CPP address, and fixup xpb_addr as needed,
 * based upon NFP model.
 */
static uint32_t
nfp_xpb_to_cpp(struct nfp_cpp *cpp, uint32_t *xpb_addr)
{
	uint32_t xpb;
	int island;

	if (!NFP_CPP_MODEL_IS_6000(cpp->model))
		return 0;

	xpb = NFP_CPP_ID(14, NFP_CPP_ACTION_RW, 0);

	/*
	 * Ensure that non-local XPB accesses go out through the
	 * global XPBM bus.
	 */
	island = ((*xpb_addr) >> 24) & 0x3f;

	if (!island)
		return xpb;

	if (island == 1) {
		/*
		 * Accesses to the ARM Island overlay uses Island 0
		 * Global Bit
		 */
		(*xpb_addr) &= ~0x7f000000;
		if (*xpb_addr < 0x60000)
			*xpb_addr |= (1 << 30);
		else
			/* And only non-ARM interfaces use island id = 1 */
			if (NFP_CPP_INTERFACE_TYPE_of(nfp_cpp_interface(cpp)) !=
			    NFP_CPP_INTERFACE_TYPE_ARM)
				*xpb_addr |= (1 << 24);
	} else {
		(*xpb_addr) |= (1 << 30);
	}

	return xpb;
}

int
nfp_cpp_area_readl(struct nfp_cpp_area *area, unsigned long offset,
		   uint32_t *value)
{
	int sz;
	uint32_t tmp = 0;

	sz = nfp_cpp_area_read(area, offset, &tmp, sizeof(tmp));
	*value = rte_le_to_cpu_32(tmp);

	return (sz == sizeof(*value)) ? 0 : -1;
}

int
nfp_cpp_area_writel(struct nfp_cpp_area *area, unsigned long offset,
		    uint32_t value)
{
	int sz;

	value = rte_cpu_to_le_32(value);
	sz = nfp_cpp_area_write(area, offset, &value, sizeof(value));
	return (sz == sizeof(value)) ? 0 : -1;
}

int
nfp_cpp_area_readq(struct nfp_cpp_area *area, unsigned long offset,
		   uint64_t *value)
{
	int sz;
	uint64_t tmp = 0;

	sz = nfp_cpp_area_read(area, offset, &tmp, sizeof(tmp));
	*value = rte_le_to_cpu_64(tmp);

	return (sz == sizeof(*value)) ? 0 : -1;
}

int
nfp_cpp_area_writeq(struct nfp_cpp_area *area, unsigned long offset,
		    uint64_t value)
{
	int sz;

	value = rte_cpu_to_le_64(value);
	sz = nfp_cpp_area_write(area, offset, &value, sizeof(value));

	return (sz == sizeof(value)) ? 0 : -1;
}

int
nfp_cpp_readl(struct nfp_cpp *cpp, uint32_t cpp_id, unsigned long long address,
	      uint32_t *value)
{
	int sz;
	uint32_t tmp;

	sz = nfp_cpp_read(cpp, cpp_id, address, &tmp, sizeof(tmp));
	*value = rte_le_to_cpu_32(tmp);

	return (sz == sizeof(*value)) ? 0 : -1;
}

int
nfp_cpp_writel(struct nfp_cpp *cpp, uint32_t cpp_id, unsigned long long address,
	       uint32_t value)
{
	int sz;

	value = rte_cpu_to_le_32(value);
	sz = nfp_cpp_write(cpp, cpp_id, address, &value, sizeof(value));

	return (sz == sizeof(value)) ? 0 : -1;
}

int
nfp_cpp_readq(struct nfp_cpp *cpp, uint32_t cpp_id, unsigned long long address,
	      uint64_t *value)
{
	int sz;
	uint64_t tmp;

	sz = nfp_cpp_read(cpp, cpp_id, address, &tmp, sizeof(tmp));
	*value = rte_le_to_cpu_64(tmp);

	return (sz == sizeof(*value)) ? 0 : -1;
}

int
nfp_cpp_writeq(struct nfp_cpp *cpp, uint32_t cpp_id, unsigned long long address,
	       uint64_t value)
{
	int sz;

	value = rte_cpu_to_le_64(value);
	sz = nfp_cpp_write(cpp, cpp_id, address, &value, sizeof(value));

	return (sz == sizeof(value)) ? 0 : -1;
}

int
nfp_xpb_writel(struct nfp_cpp *cpp, uint32_t xpb_addr, uint32_t value)
{
	uint32_t cpp_dest;

	cpp_dest = nfp_xpb_to_cpp(cpp, &xpb_addr);

	return nfp_cpp_writel(cpp, cpp_dest, xpb_addr, value);
}

int
nfp_xpb_readl(struct nfp_cpp *cpp, uint32_t xpb_addr, uint32_t *value)
{
	uint32_t cpp_dest;

	cpp_dest = nfp_xpb_to_cpp(cpp, &xpb_addr);

	return nfp_cpp_readl(cpp, cpp_dest, xpb_addr, value);
}

static struct nfp_cpp *
nfp_cpp_alloc(struct rte_pci_device *dev, int driver_lock_needed)
{
	const struct nfp_cpp_operations *ops;
	struct nfp_cpp *cpp;
	int err;

	ops = nfp_cpp_transport_operations();

	if (!ops || !ops->init)
		return NFP_ERRPTR(EINVAL);

	cpp = calloc(1, sizeof(*cpp));
	if (!cpp)
		return NULL;

	cpp->op = ops;
	cpp->driver_lock_needed = driver_lock_needed;

	if (cpp->op->init) {
		err = cpp->op->init(cpp, dev);
		if (err < 0) {
			free(cpp);
			return NULL;
		}
	}

	if (NFP_CPP_MODEL_IS_6000(nfp_cpp_model(cpp))) {
		uint32_t xpbaddr;
		size_t tgt;

		for (tgt = 0; tgt < ARRAY_SIZE(cpp->imb_cat_table); tgt++) {
			/* Hardcoded XPB IMB Base, island 0 */
			xpbaddr = 0x000a0000 + (tgt * 4);
			err = nfp_xpb_readl(cpp, xpbaddr,
				(uint32_t *)&cpp->imb_cat_table[tgt]);
			if (err < 0) {
				free(cpp);
				return NULL;
			}
		}
	}

	return cpp;
}

/*
 * nfp_cpp_free - free the CPP handle
 * @cpp:    CPP handle
 */
void
nfp_cpp_free(struct nfp_cpp *cpp)
{
	if (cpp->op && cpp->op->free)
		cpp->op->free(cpp);

	if (cpp->serial_len)
		free(cpp->serial);

	free(cpp);
}

struct nfp_cpp *
nfp_cpp_from_device_name(struct rte_pci_device *dev, int driver_lock_needed)
{
	return nfp_cpp_alloc(dev, driver_lock_needed);
}

/*
 * Modify bits of a 32-bit value from the XPB bus
 *
 * @param cpp           NFP CPP device handle
 * @param xpb_tgt       XPB target and address
 * @param mask          mask of bits to alter
 * @param value         value to modify
 *
 * @return 0 on success, or -1 on failure (and set errno accordingly).
 */
int
nfp_xpb_writelm(struct nfp_cpp *cpp, uint32_t xpb_tgt, uint32_t mask,
		uint32_t value)
{
	int err;
	uint32_t tmp;

	err = nfp_xpb_readl(cpp, xpb_tgt, &tmp);
	if (err < 0)
		return err;

	tmp &= ~mask;
	tmp |= (mask & value);
	return nfp_xpb_writel(cpp, xpb_tgt, tmp);
}

/*
 * Modify bits of a 32-bit value from the XPB bus
 *
 * @param cpp           NFP CPP device handle
 * @param xpb_tgt       XPB target and address
 * @param mask          mask of bits to alter
 * @param value         value to monitor for
 * @param timeout_us    maximum number of us to wait (-1 for forever)
 *
 * @return >= 0 on success, or -1 on failure (and set errno accordingly).
 */
int
nfp_xpb_waitlm(struct nfp_cpp *cpp, uint32_t xpb_tgt, uint32_t mask,
	       uint32_t value, int timeout_us)
{
	uint32_t tmp;
	int err;

	do {
		err = nfp_xpb_readl(cpp, xpb_tgt, &tmp);
		if (err < 0)
			goto exit;

		if ((tmp & mask) == (value & mask)) {
			if (timeout_us < 0)
				timeout_us = 0;
			break;
		}

		if (timeout_us < 0)
			continue;

		timeout_us -= 100;
		usleep(100);
	} while (timeout_us >= 0);

	if (timeout_us < 0)
		err = NFP_ERRNO(ETIMEDOUT);
	else
		err = timeout_us;

exit:
	return err;
}

/*
 * nfp_cpp_read - read from CPP target
 * @cpp:        CPP handle
 * @destination:    CPP id
 * @address:        offset into CPP target
 * @kernel_vaddr:   kernel buffer for result
 * @length:     number of bytes to read
 */
int
nfp_cpp_read(struct nfp_cpp *cpp, uint32_t destination,
	     unsigned long long address, void *kernel_vaddr, size_t length)
{
	struct nfp_cpp_area *area;
	int err;

	area = nfp_cpp_area_alloc_acquire(cpp, destination, address, length);
	if (!area) {
		printf("Area allocation/acquire failed\n");
		return -1;
	}

	err = nfp_cpp_area_read(area, 0, kernel_vaddr, length);

	nfp_cpp_area_release_free(area);
	return err;
}

/*
 * nfp_cpp_write - write to CPP target
 * @cpp:        CPP handle
 * @destination:    CPP id
 * @address:        offset into CPP target
 * @kernel_vaddr:   kernel buffer to read from
 * @length:     number of bytes to write
 */
int
nfp_cpp_write(struct nfp_cpp *cpp, uint32_t destination,
	      unsigned long long address, const void *kernel_vaddr,
	      size_t length)
{
	struct nfp_cpp_area *area;
	int err;

	area = nfp_cpp_area_alloc_acquire(cpp, destination, address, length);
	if (!area)
		return -1;

	err = nfp_cpp_area_write(area, 0, kernel_vaddr, length);

	nfp_cpp_area_release_free(area);
	return err;
}

/*
 * nfp_cpp_area_fill - fill a CPP area with a value
 * @area:       CPP area
 * @offset:     offset into CPP area
 * @value:      value to fill with
 * @length:     length of area to fill
 */
int
nfp_cpp_area_fill(struct nfp_cpp_area *area, unsigned long offset,
		  uint32_t value, size_t length)
{
	int err;
	size_t i;
	uint64_t value64;

	value = rte_cpu_to_le_32(value);
	value64 = ((uint64_t)value << 32) | value;

	if ((offset + length) > area->size)
		return NFP_ERRNO(EINVAL);

	if ((area->offset + offset) & 3)
		return NFP_ERRNO(EINVAL);

	if (((area->offset + offset) & 7) == 4 && length >= 4) {
		err = nfp_cpp_area_write(area, offset, &value, sizeof(value));
		if (err < 0)
			return err;
		if (err != sizeof(value))
			return NFP_ERRNO(ENOSPC);
		offset += sizeof(value);
		length -= sizeof(value);
	}

	for (i = 0; (i + sizeof(value)) < length; i += sizeof(value64)) {
		err =
		    nfp_cpp_area_write(area, offset + i, &value64,
				       sizeof(value64));
		if (err < 0)
			return err;
		if (err != sizeof(value64))
			return NFP_ERRNO(ENOSPC);
	}

	if ((i + sizeof(value)) <= length) {
		err =
		    nfp_cpp_area_write(area, offset + i, &value, sizeof(value));
		if (err < 0)
			return err;
		if (err != sizeof(value))
			return NFP_ERRNO(ENOSPC);
		i += sizeof(value);
	}

	return (int)i;
}

/*
 * NOTE: This code should not use nfp_xpb_* functions,
 * as those are model-specific
 */
uint32_t
__nfp_cpp_model_autodetect(struct nfp_cpp *cpp)
{
	uint32_t arm_id = NFP_CPP_ID(NFP_CPP_TARGET_ARM, 0, 0);
	uint32_t model = 0;

	if (nfp_cpp_readl(cpp, arm_id, NFP6000_ARM_GCSR_SOFTMODEL0, &model))
		return 0;

	if (NFP_CPP_MODEL_IS_6000(model)) {
		uint32_t tmp;

		nfp_cpp_model_set(cpp, model);

		/* The PL's PluDeviceID revision code is authoratative */
		model &= ~0xff;
		if (nfp_xpb_readl(cpp, NFP_XPB_DEVICE(1, 1, 16) +
				   NFP_PL_DEVICE_ID, &tmp))
			return 0;

		model |= (NFP_PL_DEVICE_ID_MASK & tmp) - 0x10;
	}

	return model;
}

/*
 * nfp_cpp_map_area() - Helper function to map an area
 * @cpp:    NFP CPP handler
 * @domain: CPP domain
 * @target: CPP target
 * @addr:   CPP address
 * @size:   Size of the area
 * @area:   Area handle (output)
 *
 * Map an area of IOMEM access.  To undo the effect of this function call
 * @nfp_cpp_area_release_free(*area).
 *
 * Return: Pointer to memory mapped area or ERR_PTR
 */
uint8_t *
nfp_cpp_map_area(struct nfp_cpp *cpp, int domain, int target, uint64_t addr,
		 unsigned long size, struct nfp_cpp_area **area)
{
	uint8_t *res;
	uint32_t dest;

	dest = NFP_CPP_ISLAND_ID(target, NFP_CPP_ACTION_RW, 0, domain);

	*area = nfp_cpp_area_alloc_acquire(cpp, dest, addr, size);
	if (!*area)
		goto err_eio;

	res = nfp_cpp_area_iomem(*area);
	if (!res)
		goto err_release_free;

	return res;

err_release_free:
	nfp_cpp_area_release_free(*area);
err_eio:
	return NULL;
}
