/*
 * Copyright (c) 2015 - 2017, Xilinx Inc. and Contributors. All rights reserved.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

/*
 * @file	io.h
 * @brief	I/O access primitives for libmetal.
 */

#ifndef __METAL_IO__H__
#define __METAL_IO__H__

#include <limits.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <metal/assert.h>
#include <metal/compiler.h>
#include <metal/atomic.h>
#include <metal/sys.h>
#include <metal/cpu.h>

#ifdef __cplusplus
extern "C" {
#endif

/** \defgroup io IO Interfaces
 *  @{
 */

#ifdef __MICROBLAZE__
#define NO_ATOMIC_64_SUPPORT
#endif

struct metal_io_region;

/** Generic I/O operations. */
struct metal_io_ops {
	uint64_t	(*read)(struct metal_io_region *io,
				unsigned long offset,
				memory_order order,
				int width);
	void		(*write)(struct metal_io_region *io,
				 unsigned long offset,
				 uint64_t value,
				 memory_order order,
				 int width);
	int		(*block_read)(struct metal_io_region *io,
				      unsigned long offset,
				      void *restrict dst,
				      memory_order order,
				      int len);
	int		(*block_write)(struct metal_io_region *io,
				       unsigned long offset,
				       const void *restrict src,
				       memory_order order,
				       int len);
	void		(*block_set)(struct metal_io_region *io,
				     unsigned long offset,
				     unsigned char value,
				     memory_order order,
				     int len);
	void		(*close)(struct metal_io_region *io);
	metal_phys_addr_t (*offset_to_phys)(struct metal_io_region *io,
					    unsigned long offset);
	unsigned long	(*phys_to_offset)(struct metal_io_region *io,
					  metal_phys_addr_t phys);
};

/** Libmetal I/O region structure. */
struct metal_io_region {
	void			*virt;      /**< base virtual address */
	const metal_phys_addr_t	*physmap;   /**< table of base physical address
						 of each of the pages in the I/O
						 region */
	size_t			size;       /**< size of the I/O region */
	unsigned long		page_shift; /**< page shift of I/O region */
	metal_phys_addr_t	page_mask;  /**< page mask of I/O region */
	unsigned int		mem_flags;  /**< memory attribute of the
						 I/O region */
	struct metal_io_ops	ops;        /**< I/O region operations */
	struct metal_list	list;       /**< linked list */
};

/**
 * @brief	Open a libmetal I/O region.
 *
 * @param[in, out]	io		I/O region handle.
 * @param[in]		virt		Virtual address of region.
 * @param[in]		physmap		Array of physical addresses per page.
 * @param[in]		size		Size of region.
 * @param[in]		page_shift	Log2 of page size (-1 for single page).
 * @param[in]		mem_flags	Memory flags
 * @param[in]		ops			ops
 */
void
metal_io_init(struct metal_io_region *io, void *virt,
	      const metal_phys_addr_t *physmap, size_t size,
	      unsigned int page_shift, unsigned int mem_flags,
	      const struct metal_io_ops *ops);

/**
 * @brief	Close a libmetal shared memory segment.
 * @param[in]	io	I/O region handle.
 */
static inline void metal_io_finish(struct metal_io_region *io)
{
	if (io->ops.close)
		(*io->ops.close)(io);
	memset(io, 0, sizeof(*io));
}

/**
 * @brief	Get size of I/O region.
 *
 * @param[in]	io	I/O region handle.
 * @return	Size of I/O region.
 */
static inline size_t metal_io_region_size(struct metal_io_region *io)
{
	return io->size;
}

/**
 * @brief	Get virtual address for a given offset into the I/O region.
 * @param[in]	io	I/O region handle.
 * @param[in]	offset	Offset into shared memory segment.
 * @return	NULL if offset is out of range, or pointer to offset.
 */
static inline void *
metal_io_virt(struct metal_io_region *io, unsigned long offset)
{
	return (io->virt != METAL_BAD_VA && offset < io->size
		? (void *)((uintptr_t)io->virt + offset)
		: NULL);
}

/**
 * @brief	Convert a virtual address to offset within I/O region.
 * @param[in]	io	I/O region handle.
 * @param[in]	virt	Virtual address within segment.
 * @return	METAL_BAD_OFFSET if out of range, or offset.
 */
static inline unsigned long
metal_io_virt_to_offset(struct metal_io_region *io, void *virt)
{
	size_t offset = (uintptr_t)virt - (uintptr_t)io->virt;

	return (offset < io->size ? offset : METAL_BAD_OFFSET);
}

/**
 * @brief	Get physical address for a given offset into the I/O region.
 * @param[in]	io	I/O region handle.
 * @param[in]	offset	Offset into shared memory segment.
 * @return	METAL_BAD_PHYS if offset is out of range, or physical address
 *		of offset.
 */
static inline metal_phys_addr_t
metal_io_phys(struct metal_io_region *io, unsigned long offset)
{
	if (!io->ops.offset_to_phys) {
		unsigned long page = (io->page_shift >=
				     sizeof(offset) * CHAR_BIT ?
				     0 : offset >> io->page_shift);
		return (io->physmap && offset < io->size
			? io->physmap[page] + (offset & io->page_mask)
			: METAL_BAD_PHYS);
	}

	return io->ops.offset_to_phys(io, offset);
}

/**
 * @brief	Convert a physical address to offset within I/O region.
 * @param[in]	io	I/O region handle.
 * @param[in]	phys	Physical address within segment.
 * @return	METAL_BAD_OFFSET if out of range, or offset.
 */
static inline unsigned long
metal_io_phys_to_offset(struct metal_io_region *io, metal_phys_addr_t phys)
{
	if (!io->ops.phys_to_offset) {
		unsigned long offset =
			(io->page_mask == (metal_phys_addr_t)(-1) ?
			phys - io->physmap[0] :  phys & io->page_mask);
		do {
			if (metal_io_phys(io, offset) == phys)
				return offset;
			offset += io->page_mask + 1;
		} while (offset < io->size);
		return METAL_BAD_OFFSET;
	}

	return (*io->ops.phys_to_offset)(io, phys);
}

/**
 * @brief	Convert a physical address to virtual address.
 * @param[in]	io	Shared memory segment handle.
 * @param[in]	phys	Physical address within segment.
 * @return	NULL if out of range, or corresponding virtual address.
 */
static inline void *
metal_io_phys_to_virt(struct metal_io_region *io, metal_phys_addr_t phys)
{
	return metal_io_virt(io, metal_io_phys_to_offset(io, phys));
}

/**
 * @brief	Convert a virtual address to physical address.
 * @param[in]	io	Shared memory segment handle.
 * @param[in]	virt	Virtual address within segment.
 * @return	METAL_BAD_PHYS if out of range, or corresponding
 *		physical address.
 */
static inline metal_phys_addr_t
metal_io_virt_to_phys(struct metal_io_region *io, void *virt)
{
	return metal_io_phys(io, metal_io_virt_to_offset(io, virt));
}

/**
 * @brief	Read a value from an I/O region.
 * @param[in]	io	I/O region handle.
 * @param[in]	offset	Offset into I/O region.
 * @param[in]	order	Memory ordering.
 * @param[in]	width	Width in bytes of datatype to read.  This must be 1, 2,
 *			4, or 8, and a compile time constant for this function
 *			to inline cleanly.
 * @return	Value.
 */
static inline uint64_t
metal_io_read(struct metal_io_region *io, unsigned long offset,
	      memory_order order, int width)
{
	void *ptr = metal_io_virt(io, offset);

	if (io->ops.read)
		return (*io->ops.read)(io, offset, order, width);
	else if (ptr && sizeof(atomic_uchar) == width)
		return atomic_load_explicit((atomic_uchar *)ptr, order);
	else if (ptr && sizeof(atomic_ushort) == width)
		return atomic_load_explicit((atomic_ushort *)ptr, order);
	else if (ptr && sizeof(atomic_uint) == width)
		return atomic_load_explicit((atomic_uint *)ptr, order);
	else if (ptr && sizeof(atomic_ulong) == width)
		return atomic_load_explicit((atomic_ulong *)ptr, order);
#ifndef NO_ATOMIC_64_SUPPORT
	else if (ptr && sizeof(atomic_ullong) == width)
		return atomic_load_explicit((atomic_ullong *)ptr, order);
#endif
	metal_assert(0);
	return 0; /* quiet compiler */
}

/**
 * @brief	Write a value into an I/O region.
 * @param[in]	io	I/O region handle.
 * @param[in]	offset	Offset into I/O region.
 * @param[in]	value	Value to write.
 * @param[in]	order	Memory ordering.
 * @param[in]	width	Width in bytes of datatype to read.  This must be 1, 2,
 *			4, or 8, and a compile time constant for this function
 *			to inline cleanly.
 */
static inline void
metal_io_write(struct metal_io_region *io, unsigned long offset,
	       uint64_t value, memory_order order, int width)
{
	void *ptr = metal_io_virt(io, offset);

	if (io->ops.write)
		(*io->ops.write)(io, offset, value, order, width);
	else if (ptr && sizeof(atomic_uchar) == width)
		atomic_store_explicit((atomic_uchar *)ptr, (unsigned char)value,
				      order);
	else if (ptr && sizeof(atomic_ushort) == width)
		atomic_store_explicit((atomic_ushort *)ptr,
				      (unsigned short)value, order);
	else if (ptr && sizeof(atomic_uint) == width)
		atomic_store_explicit((atomic_uint *)ptr, (unsigned int)value,
				      order);
	else if (ptr && sizeof(atomic_ulong) == width)
		atomic_store_explicit((atomic_ulong *)ptr, (unsigned long)value,
				      order);
#ifndef NO_ATOMIC_64_SUPPORT
	else if (ptr && sizeof(atomic_ullong) == width)
		atomic_store_explicit((atomic_ullong *)ptr,
				      (unsigned long long)value, order);
#endif
	else
		metal_assert(0);
}

#define metal_io_read8_explicit(_io, _ofs, _order)			\
	metal_io_read((_io), (_ofs), (_order), 1)
#define metal_io_read8(_io, _ofs)					\
	metal_io_read((_io), (_ofs), memory_order_seq_cst, 1)
#define metal_io_write8_explicit(_io, _ofs, _val, _order)		\
	metal_io_write((_io), (_ofs), (_val), (_order), 1)
#define metal_io_write8(_io, _ofs, _val)				\
	metal_io_write((_io), (_ofs), (_val), memory_order_seq_cst, 1)

#define metal_io_read16_explicit(_io, _ofs, _order)			\
	metal_io_read((_io), (_ofs), (_order), 2)
#define metal_io_read16(_io, _ofs)					\
	metal_io_read((_io), (_ofs), memory_order_seq_cst, 2)
#define metal_io_write16_explicit(_io, _ofs, _val, _order)		\
	metal_io_write((_io), (_ofs), (_val), (_order), 2)
#define metal_io_write16(_io, _ofs, _val)				\
	metal_io_write((_io), (_ofs), (_val), memory_order_seq_cst, 2)

#define metal_io_read32_explicit(_io, _ofs, _order)			\
	metal_io_read((_io), (_ofs), (_order), 4)
#define metal_io_read32(_io, _ofs)					\
	metal_io_read((_io), (_ofs), memory_order_seq_cst, 4)
#define metal_io_write32_explicit(_io, _ofs, _val, _order)		\
	metal_io_write((_io), (_ofs), (_val), (_order), 4)
#define metal_io_write32(_io, _ofs, _val)				\
	metal_io_write((_io), (_ofs), (_val), memory_order_seq_cst, 4)

#define metal_io_read64_explicit(_io, _ofs, _order)			\
	metal_io_read((_io), (_ofs), (_order), 8)
#define metal_io_read64(_io, _ofs)					\
	metal_io_read((_io), (_ofs), memory_order_seq_cst, 8)
#define metal_io_write64_explicit(_io, _ofs, _val, _order)		\
	metal_io_write((_io), (_ofs), (_val), (_order), 8)
#define metal_io_write64(_io, _ofs, _val)				\
	metal_io_write((_io), (_ofs), (_val), memory_order_seq_cst, 8)

/**
 * @brief	Read a block from an I/O region.
 * @param[in]	io	I/O region handle.
 * @param[in]	offset	Offset into I/O region.
 * @param[in]	dst	destination to store the read data.
 * @param[in]	len	length in bytes to read.
 * @return      On success, number of bytes read. On failure, negative value
 */
int metal_io_block_read(struct metal_io_region *io, unsigned long offset,
			void *restrict dst, int len);

/**
 * @brief	Write a block into an I/O region.
 * @param[in]	io	I/O region handle.
 * @param[in]	offset	Offset into I/O region.
 * @param[in]	src	source to write.
 * @param[in]	len	length in bytes to write.
 * @return      On success, number of bytes written. On failure, negative value
 */
int metal_io_block_write(struct metal_io_region *io, unsigned long offset,
			 const void *restrict src, int len);

/**
 * @brief	fill a block of an I/O region.
 * @param[in]	io	I/O region handle.
 * @param[in]	offset	Offset into I/O region.
 * @param[in]	value	value to fill into the block
 * @param[in]	len	length in bytes to fill.
 * @return      On success, number of bytes filled. On failure, negative value
 */
int metal_io_block_set(struct metal_io_region *io, unsigned long offset,
		       unsigned char value, int len);

/** @} */

#ifdef __cplusplus
}
#endif

#include <metal/system/linux/io.h>

#endif /* __METAL_IO__H__ */
