/*
 * Copyright 2021 BayLibre, SAS
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(intc_gicv3_its, LOG_LEVEL_ERR);

#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/interrupt_controller/gicv3_its.h>
#include <zephyr/sys/barrier.h>

#include "intc_gic_common_priv.h"
#include "intc_gicv3_priv.h"

#define DT_DRV_COMPAT   arm_gic_v3_its

/*
 * Current ITS implementation only handle GICv3 ITS physical interruption generation
 * Implementation is designed for the PCIe MSI/MSI-X use-case in mind.
 */

#define GITS_BASER_NR_REGS              8

/* convenient access to all redistributors base address */
extern mem_addr_t gic_rdists[CONFIG_MP_MAX_NUM_CPUS];

#define SIZE_256                        256
#define SIZE_4K                         KB(4)
#define SIZE_16K                        KB(16)
#define SIZE_64K                        KB(64)

struct its_cmd_block {
	uint64_t raw_cmd[4];
};

#define ITS_CMD_QUEUE_SIZE              SIZE_64K
#define ITS_CMD_QUEUE_NR_ENTRIES        (ITS_CMD_QUEUE_SIZE / sizeof(struct its_cmd_block))

struct gicv3_its_data {
	mm_reg_t base;
	struct its_cmd_block *cmd_base;
	struct its_cmd_block *cmd_write;
	bool dev_table_is_indirect;
	uint64_t *indirect_dev_lvl1_table;
	size_t indirect_dev_lvl1_width;
	size_t indirect_dev_lvl2_width;
	size_t indirect_dev_page_size;
};

struct gicv3_its_config {
	uintptr_t base_addr;
	size_t base_size;
	struct its_cmd_block *cmd_queue;
	size_t cmd_queue_size;
};

static inline int fls_z(unsigned int x)
{
	unsigned int bits = sizeof(x) * 8;
	unsigned int cmp = 1 << (bits - 1);

	while (bits) {
		if (x & cmp) {
			return bits;
		}
		cmp >>= 1;
		bits--;
	}

	return 0;
}

/* wait 500ms & wakeup every millisecond */
#define WAIT_QUIESCENT 500

static int its_force_quiescent(struct gicv3_its_data *data)
{
	unsigned int count = WAIT_QUIESCENT;
	uint32_t reg = sys_read32(data->base + GITS_CTLR);

	if (GITS_CTLR_ENABLED_GET(reg)) {
		/* Disable ITS */
		reg &= ~MASK(GITS_CTLR_ENABLED);
		sys_write32(reg, data->base + GITS_CTLR);
	}

	while (1) {
		if (GITS_CTLR_QUIESCENT_GET(reg)) {
			return 0;
		}

		count--;
		if (!count) {
			return -EBUSY;
		}

		k_msleep(1);
		reg = sys_read32(data->base + GITS_CTLR);
	}

	return 0;
}

static const char *const its_base_type_string[] = {
	[GITS_BASER_TYPE_DEVICE] = "Devices",
	[GITS_BASER_TYPE_COLLECTION] = "Interrupt Collections",
};

/* Probe the BASER(i) to get the largest supported page size */
static size_t its_probe_baser_page_size(struct gicv3_its_data *data, int i)
{
	uint64_t page_size = GITS_BASER_PAGE_SIZE_64K;

	while (page_size > GITS_BASER_PAGE_SIZE_4K) {
		uint64_t reg = sys_read64(data->base + GITS_BASER(i));

		reg &= ~MASK(GITS_BASER_PAGE_SIZE);
		reg |= MASK_SET(page_size, GITS_BASER_PAGE_SIZE);

		sys_write64(reg, data->base + GITS_BASER(i));

		reg = sys_read64(data->base + GITS_BASER(i));

		if (MASK_GET(reg, GITS_BASER_PAGE_SIZE) == page_size) {
			break;
		}

		switch (page_size) {
		case GITS_BASER_PAGE_SIZE_64K:
			page_size = GITS_BASER_PAGE_SIZE_16K;
			break;
		default:
			page_size = GITS_BASER_PAGE_SIZE_4K;
		}
	}

	switch (page_size) {
	case GITS_BASER_PAGE_SIZE_64K:
		return SIZE_64K;
	case GITS_BASER_PAGE_SIZE_16K:
		return SIZE_16K;
	default:
		return SIZE_4K;
	}
}

static int its_alloc_tables(struct gicv3_its_data *data)
{
	unsigned int device_ids = GITS_TYPER_DEVBITS_GET(sys_read64(data->base + GITS_TYPER)) + 1;
	int i;

	for (i = 0; i < GITS_BASER_NR_REGS; ++i) {
		uint64_t reg = sys_read64(data->base + GITS_BASER(i));
		unsigned int type = GITS_BASER_TYPE_GET(reg);
		size_t page_size, entry_size, page_cnt, lvl2_width = 0;
		bool indirect = false;
		void *alloc_addr;

		entry_size = GITS_BASER_ENTRY_SIZE_GET(reg) + 1;

		switch (GITS_BASER_PAGE_SIZE_GET(reg)) {
		case GITS_BASER_PAGE_SIZE_4K:
			page_size = SIZE_4K;
			break;
		case GITS_BASER_PAGE_SIZE_16K:
			page_size = SIZE_16K;
			break;
		case GITS_BASER_PAGE_SIZE_64K:
			page_size = SIZE_64K;
			break;
		default:
			page_size = SIZE_4K;
		}

		switch (type) {
		case GITS_BASER_TYPE_DEVICE:
			if (device_ids > 16) {
				/* Use the largest possible page size for indirect */
				page_size = its_probe_baser_page_size(data, i);

				/*
				 * lvl1 table size:
				 * subtract ID bits that sparse lvl2 table from 'ids'
				 * which is reported by ITS hardware times lvl1 table
				 * entry size.
				 */
				lvl2_width = fls_z(page_size / entry_size) - 1;
				device_ids -= lvl2_width + 1;

				/* The level 1 entry size is a 64bit pointer */
				entry_size = sizeof(uint64_t);

				indirect = true;
			}

			page_cnt = ROUND_UP(entry_size << device_ids, page_size) / page_size;
			break;
		case GITS_BASER_TYPE_COLLECTION:
			page_cnt =
				ROUND_UP(entry_size * CONFIG_MP_MAX_NUM_CPUS, page_size)/page_size;
			break;
		default:
			continue;
		}

		LOG_INF("Allocating %s table of %ldx%ldK pages (%ld bytes entry)",
			its_base_type_string[type], page_cnt, page_size / 1024, entry_size);

		alloc_addr = k_aligned_alloc(page_size, page_size * page_cnt);
		if (!alloc_addr) {
			return -ENOMEM;
		}

		memset(alloc_addr, 0, page_size * page_cnt);

		switch (page_size) {
		case SIZE_4K:
			reg = MASK_SET(GITS_BASER_PAGE_SIZE_4K, GITS_BASER_PAGE_SIZE);
			break;
		case SIZE_16K:
			reg = MASK_SET(GITS_BASER_PAGE_SIZE_16K, GITS_BASER_PAGE_SIZE);
			break;
		case SIZE_64K:
			reg = MASK_SET(GITS_BASER_PAGE_SIZE_64K, GITS_BASER_PAGE_SIZE);
			break;
		}

		reg |= MASK_SET(page_cnt - 1, GITS_BASER_SIZE);
		reg |= MASK_SET(GIC_BASER_SHARE_INNER, GITS_BASER_SHAREABILITY);
		reg |= MASK_SET((uintptr_t)alloc_addr >> GITS_BASER_ADDR_SHIFT, GITS_BASER_ADDR);
		reg |= MASK_SET(GIC_BASER_CACHE_INNERLIKE, GITS_BASER_OUTER_CACHE);
		reg |= MASK_SET(GIC_BASER_CACHE_RAWAWB, GITS_BASER_INNER_CACHE);
		reg |= MASK_SET(indirect ? 1 : 0, GITS_BASER_INDIRECT);
		reg |= MASK_SET(1, GITS_BASER_VALID);

		sys_write64(reg, data->base + GITS_BASER(i));

		/* TOFIX: check page size & SHAREABILITY validity after write */

		if (type == GITS_BASER_TYPE_DEVICE && indirect) {
			data->dev_table_is_indirect = indirect;
			data->indirect_dev_lvl1_table = alloc_addr;
			data->indirect_dev_lvl1_width = device_ids;
			data->indirect_dev_lvl2_width = lvl2_width;
			data->indirect_dev_page_size = page_size;
			LOG_DBG("%s table Indirection enabled", its_base_type_string[type]);
		}
	}

	return 0;
}

static bool its_queue_full(struct gicv3_its_data *data)
{
	int widx;
	int ridx;

	widx = data->cmd_write - data->cmd_base;
	ridx = sys_read32(data->base + GITS_CREADR) / sizeof(struct its_cmd_block);

	/* This is incredibly unlikely to happen, unless the ITS locks up. */
	return (((widx + 1) % ITS_CMD_QUEUE_NR_ENTRIES) == ridx);
}

static struct its_cmd_block *its_allocate_entry(struct gicv3_its_data *data)
{
	struct its_cmd_block *cmd;
	unsigned int count = 1000000;   /* 1s! */

	while (its_queue_full(data)) {
		count--;
		if (!count) {
			LOG_ERR("ITS queue not draining");
			return NULL;
		}
		k_usleep(1);
	}

	cmd = data->cmd_write++;

	/* Handle queue wrapping */
	if (data->cmd_write == (data->cmd_base + ITS_CMD_QUEUE_NR_ENTRIES)) {
		data->cmd_write = data->cmd_base;
	}

	/* Clear command  */
	cmd->raw_cmd[0] = 0;
	cmd->raw_cmd[1] = 0;
	cmd->raw_cmd[2] = 0;
	cmd->raw_cmd[3] = 0;

	return cmd;
}

static int its_post_command(struct gicv3_its_data *data, struct its_cmd_block *cmd)
{
	uint64_t wr_idx, rd_idx, idx;
	unsigned int count = 1000000;   /* 1s! */

	wr_idx = (data->cmd_write - data->cmd_base) * sizeof(*cmd);
	rd_idx = sys_read32(data->base + GITS_CREADR);

	barrier_dsync_fence_full();

	sys_write32(wr_idx, data->base + GITS_CWRITER);

	while (1) {
		idx = sys_read32(data->base + GITS_CREADR);

		if (idx == wr_idx) {
			break;
		}

		count--;
		if (!count) {
			LOG_ERR("ITS queue timeout (rd %lld => %lld => wr %lld)",
				rd_idx, idx, wr_idx);
			return -ETIMEDOUT;
		}
		k_usleep(1);
	}

	return 0;
}

static int its_send_sync_cmd(struct gicv3_its_data *data, uintptr_t rd_addr)
{
	struct its_cmd_block *cmd = its_allocate_entry(data);

	if (!cmd) {
		return -EBUSY;
	}

	cmd->raw_cmd[0] = MASK_SET(GITS_CMD_ID_SYNC, GITS_CMD_ID);
	cmd->raw_cmd[2] = MASK_SET(rd_addr >> GITS_CMD_RDBASE_ALIGN, GITS_CMD_RDBASE);

	return its_post_command(data, cmd);
}

static int its_send_mapc_cmd(struct gicv3_its_data *data, uint32_t icid,
			     uintptr_t rd_addr, bool valid)
{
	struct its_cmd_block *cmd = its_allocate_entry(data);

	if (!cmd) {
		return -EBUSY;
	}

	cmd->raw_cmd[0] = MASK_SET(GITS_CMD_ID_MAPC, GITS_CMD_ID);
	cmd->raw_cmd[2] = MASK_SET(icid, GITS_CMD_ICID) |
			  MASK_SET(rd_addr >> GITS_CMD_RDBASE_ALIGN, GITS_CMD_RDBASE) |
			  MASK_SET(valid ? 1 : 0, GITS_CMD_VALID);

	return its_post_command(data, cmd);
}

static int its_send_mapd_cmd(struct gicv3_its_data *data, uint32_t device_id,
			     uint32_t size, uintptr_t itt_addr, bool valid)
{
	struct its_cmd_block *cmd = its_allocate_entry(data);

	if (!cmd) {
		return -EBUSY;
	}

	cmd->raw_cmd[0] = MASK_SET(GITS_CMD_ID_MAPD, GITS_CMD_ID) |
			  MASK_SET(device_id, GITS_CMD_DEVICEID);
	cmd->raw_cmd[1] = MASK_SET(size, GITS_CMD_SIZE);
	cmd->raw_cmd[2] = MASK_SET(itt_addr >> GITS_CMD_ITTADDR_ALIGN, GITS_CMD_ITTADDR) |
			  MASK_SET(valid ? 1 : 0, GITS_CMD_VALID);

	return its_post_command(data, cmd);
}

static int its_send_mapti_cmd(struct gicv3_its_data *data, uint32_t device_id,
			      uint32_t event_id, uint32_t intid, uint32_t icid)
{
	struct its_cmd_block *cmd = its_allocate_entry(data);

	if (!cmd) {
		return -EBUSY;
	}

	cmd->raw_cmd[0] = MASK_SET(GITS_CMD_ID_MAPTI, GITS_CMD_ID) |
			  MASK_SET(device_id, GITS_CMD_DEVICEID);
	cmd->raw_cmd[1] = MASK_SET(event_id, GITS_CMD_EVENTID) |
			  MASK_SET(intid, GITS_CMD_PINTID);
	cmd->raw_cmd[2] = MASK_SET(icid, GITS_CMD_ICID);

	return its_post_command(data, cmd);
}

static int its_send_int_cmd(struct gicv3_its_data *data, uint32_t device_id,
			    uint32_t event_id)
{
	struct its_cmd_block *cmd = its_allocate_entry(data);

	if (!cmd) {
		return -EBUSY;
	}

	cmd->raw_cmd[0] = MASK_SET(GITS_CMD_ID_INT, GITS_CMD_ID) |
			  MASK_SET(device_id, GITS_CMD_DEVICEID);
	cmd->raw_cmd[1] = MASK_SET(event_id, GITS_CMD_EVENTID);

	return its_post_command(data, cmd);
}

static int its_send_invall_cmd(struct gicv3_its_data *data, uint32_t icid)
{
	struct its_cmd_block *cmd = its_allocate_entry(data);

	if (!cmd) {
		return -EBUSY;
	}

	cmd->raw_cmd[0] = MASK_SET(GITS_CMD_ID_INVALL, GITS_CMD_ID);
	cmd->raw_cmd[2] = MASK_SET(icid, GITS_CMD_ICID);

	return its_post_command(data, cmd);
}

static int gicv3_its_send_int(const struct device *dev, uint32_t device_id, uint32_t event_id)
{
	struct gicv3_its_data *data = dev->data;

	/* TOFIX check device_id & event_id bounds */

	return its_send_int_cmd(data, device_id, event_id);
}

static void its_setup_cmd_queue(const struct device *dev)
{
	const struct gicv3_its_config *cfg = dev->config;
	struct gicv3_its_data *data = dev->data;
	uint64_t reg = 0;

	/* Zero out cmd table */
	memset(cfg->cmd_queue, 0, cfg->cmd_queue_size);

	reg |= MASK_SET(cfg->cmd_queue_size / SIZE_4K, GITS_CBASER_SIZE);
	reg |= MASK_SET(GIC_BASER_SHARE_INNER, GITS_CBASER_SHAREABILITY);
	reg |= MASK_SET((uintptr_t)cfg->cmd_queue >> GITS_CBASER_ADDR_SHIFT, GITS_CBASER_ADDR);
	reg |= MASK_SET(GIC_BASER_CACHE_RAWAWB, GITS_CBASER_OUTER_CACHE);
	reg |= MASK_SET(GIC_BASER_CACHE_RAWAWB, GITS_CBASER_INNER_CACHE);
	reg |= MASK_SET(1, GITS_CBASER_VALID);

	sys_write64(reg, data->base + GITS_CBASER);

	data->cmd_base = (struct its_cmd_block *)cfg->cmd_queue;
	data->cmd_write = data->cmd_base;

	LOG_INF("Allocated %ld entries for command table", ITS_CMD_QUEUE_NR_ENTRIES);

	sys_write64(0, data->base + GITS_CWRITER);
}

static uintptr_t gicv3_rdist_get_rdbase(const struct device *dev, unsigned int cpuid)
{
	struct gicv3_its_data *data = dev->data;
	uint64_t typer = sys_read64(data->base + GITS_TYPER);

	if (GITS_TYPER_PTA_GET(typer)) {
		return gic_rdists[cpuid];
	} else {
		return GICR_TYPER_PROCESSOR_NUMBER_GET(sys_read64(gic_rdists[cpuid] + GICR_TYPER));
	}
}

static int gicv3_its_map_intid(const struct device *dev, uint32_t device_id, uint32_t event_id,
			       unsigned int intid)
{
	struct gicv3_its_data *data = dev->data;
	int ret;

	/* TOFIX check device_id, event_id & intid bounds */

	if (intid < 8192) {
		return -EINVAL;
	}

	/* The CPU id directly maps as ICID for the current CPU redistributor */
	ret = its_send_mapti_cmd(data, device_id, event_id, intid, arch_curr_cpu()->id);
	if (ret) {
		LOG_ERR("Failed to map eventid %d to intid %d for deviceid %x",
			event_id, intid, device_id);
		return ret;
	}

	return its_send_sync_cmd(data, gicv3_rdist_get_rdbase(dev, arch_curr_cpu()->id));
}

static int gicv3_its_init_device_id(const struct device *dev, uint32_t device_id,
				    unsigned int nites)
{
	struct gicv3_its_data *data = dev->data;
	size_t entry_size, alloc_size;
	int nr_ites;
	void *itt;
	int ret;

	/* TOFIX check device_id & nites bounds */

	entry_size = GITS_TYPER_ITT_ENTRY_SIZE_GET(sys_read64(data->base + GITS_TYPER)) + 1;

	if (data->dev_table_is_indirect) {
		size_t offset = device_id >> data->indirect_dev_lvl2_width;

		/* Check if DeviceID can fit in the Level 1 table */
		if (offset > (1 << data->indirect_dev_lvl1_width)) {
			return -EINVAL;
		}

		/* Check if a Level 2 table has already been allocated for the DeviceID */
		if (!data->indirect_dev_lvl1_table[offset]) {
			void *alloc_addr;

			LOG_INF("Allocating Level 2 Device %ldK table",
				data->indirect_dev_page_size / 1024);

			alloc_addr = k_aligned_alloc(data->indirect_dev_page_size,
						     data->indirect_dev_page_size);
			if (!alloc_addr) {
				return -ENOMEM;
			}

			memset(alloc_addr, 0, data->indirect_dev_page_size);

			data->indirect_dev_lvl1_table[offset] = (uintptr_t)alloc_addr |
								MASK_SET(1, GITS_BASER_VALID);

			barrier_dsync_fence_full();
		}
	}

	/* ITT must be of power of 2 */
	nr_ites = MAX(2, nites);
	alloc_size = ROUND_UP(nr_ites * entry_size, 256);

	LOG_INF("Allocating ITT for DeviceID %x and %d vectors (%ld bytes entry)",
		device_id, nr_ites, entry_size);

	itt = k_aligned_alloc(256, alloc_size);
	if (!itt) {
		return -ENOMEM;
	}

	/* size is log2(ites) - 1, equivalent to (fls(ites) - 1) - 1 */
	ret = its_send_mapd_cmd(data, device_id, fls_z(nr_ites) - 2, (uintptr_t)itt, true);
	if (ret) {
		LOG_ERR("Failed to map device id %x ITT table", device_id);
		return ret;
	}

	return 0;
}

static unsigned int gicv3_its_alloc_intid(const struct device *dev)
{
	return atomic_inc(&nlpi_intid);
}

static uint32_t gicv3_its_get_msi_addr(const struct device *dev)
{
	const struct gicv3_its_config *cfg = (const struct gicv3_its_config *)dev->config;

	return cfg->base_addr + GITS_TRANSLATER;
}

#define ITS_RDIST_MAP(n)									  \
	{											  \
		const struct device *const dev = DEVICE_DT_INST_GET(n);				  \
		struct gicv3_its_data *data;							  \
		int ret;									  \
												  \
		if (dev) {									  \
			data = (struct gicv3_its_data *) dev->data;				  \
			ret = its_send_mapc_cmd(data, arch_curr_cpu()->id,			  \
						gicv3_rdist_get_rdbase(dev, arch_curr_cpu()->id), \
						true);						  \
			if (ret) {								  \
				LOG_ERR("Failed to map CPU%d redistributor",			  \
					arch_curr_cpu()->id);					  \
			}									  \
		}										  \
	}

void its_rdist_map(void)
{
	DT_INST_FOREACH_STATUS_OKAY(ITS_RDIST_MAP)
}

#define ITS_RDIST_INVALL(n)									\
	{											\
		const struct device *const dev = DEVICE_DT_INST_GET(n);				\
		struct gicv3_its_data *data;							\
		int ret;									\
												\
		if (dev) {									\
			data = (struct gicv3_its_data *) dev->data;				\
			ret = its_send_invall_cmd(data, arch_curr_cpu()->id);			\
			if (ret) {								\
				LOG_ERR("Failed to sync RDIST LPI cache for CPU%d",		\
					arch_curr_cpu()->id);					\
			}									\
												\
			its_send_sync_cmd(data,							\
					  gicv3_rdist_get_rdbase(dev, arch_curr_cpu()->id));	\
		}										\
	}

void its_rdist_invall(void)
{
	DT_INST_FOREACH_STATUS_OKAY(ITS_RDIST_INVALL)
}

static int gicv3_its_init(const struct device *dev)
{
	const struct gicv3_its_config *cfg = dev->config;
	struct gicv3_its_data *data = dev->data;
	uint32_t reg;
	int ret;

	device_map(&data->base, cfg->base_addr, cfg->base_size, K_MEM_CACHE_NONE);

	ret = its_force_quiescent(data);
	if (ret) {
		LOG_ERR("Failed to quiesce, giving up");
		return ret;
	}

	ret = its_alloc_tables(data);
	if (ret) {
		LOG_ERR("Failed to allocate tables, giving up");
		return ret;
	}

	its_setup_cmd_queue(dev);

	reg = sys_read32(data->base + GITS_CTLR);
	reg |= MASK_SET(1, GITS_CTLR_ENABLED);
	sys_write32(reg, data->base + GITS_CTLR);

	/* Map the boot CPU id to the CPU redistributor */
	ret = its_send_mapc_cmd(data, arch_curr_cpu()->id,
				gicv3_rdist_get_rdbase(dev, arch_curr_cpu()->id), true);
	if (ret) {
		LOG_ERR("Failed to map boot CPU redistributor");
		return ret;
	}

	return 0;
}

struct its_driver_api gicv3_its_api = {
	.alloc_intid = gicv3_its_alloc_intid,
	.setup_deviceid = gicv3_its_init_device_id,
	.map_intid = gicv3_its_map_intid,
	.send_int = gicv3_its_send_int,
	.get_msi_addr = gicv3_its_get_msi_addr,
};

#define GICV3_ITS_INIT(n)						       \
	static struct its_cmd_block gicv3_its_cmd##n[ITS_CMD_QUEUE_NR_ENTRIES] \
	__aligned(ITS_CMD_QUEUE_SIZE);					       \
	static struct gicv3_its_data gicv3_its_data##n;			       \
	static const struct gicv3_its_config gicv3_its_config##n = {	       \
		.base_addr = DT_INST_REG_ADDR(n),			       \
		.base_size = DT_INST_REG_SIZE(n),			       \
		.cmd_queue = gicv3_its_cmd##n,				       \
		.cmd_queue_size = sizeof(gicv3_its_cmd##n),		       \
	};								       \
	DEVICE_DT_INST_DEFINE(n, &gicv3_its_init, NULL,			       \
			      &gicv3_its_data##n,			       \
			      &gicv3_its_config##n,			       \
			      PRE_KERNEL_1,				       \
			      CONFIG_INTC_INIT_PRIORITY,		       \
			      &gicv3_its_api);

DT_INST_FOREACH_STATUS_OKAY(GICV3_ITS_INIT)
