// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
/* Copyright 2017 IBM Corp. */

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <stdarg.h>
#include <inttypes.h>

#include <sys/mman.h> /* for mprotect() */

#define pr_fmt(fmt) "MBOX-SERVER: " fmt
#include "skiboot.h"
#include "opal-api.h"

#include "mbox-server.h"
#include "stubs.h"

#define ERASE_GRANULE 0x100

#define LPC_BLOCKS 256

#define __unused          __attribute__((unused))

enum win_type {
	WIN_CLOSED,
	WIN_READ,
	WIN_WRITE
};

typedef void (*mbox_data_cb)(struct bmc_mbox_msg *msg, void *priv);
typedef void (*mbox_attn_cb)(uint8_t reg, void *priv);

struct {
	mbox_data_cb fn;
	void *cb_data;
	struct bmc_mbox_msg *msg;
	mbox_attn_cb attn;
	void *cb_attn;
} mbox_data;

static struct {
	int api;
	bool reset;

	void *lpc_base;
	size_t lpc_size;

	uint8_t attn_reg;

	uint32_t block_shift;
	uint32_t erase_granule;

	uint16_t def_read_win;  /* default window size in blocks */
	uint16_t def_write_win;

	uint16_t max_read_win; /* max window size in blocks */
	uint16_t max_write_win;

	enum win_type win_type;
	uint32_t win_base;
	uint32_t win_size;
	bool win_dirty;
} server_state;


static bool check_window(uint32_t pos, uint32_t size)
{
	/* If size is zero then all is well */
	if (size == 0)
		return true;

	if (server_state.api == 1) {
		/*
		 * Can actually be stricter in v1 because pos is relative to
		 * flash not window
		 */
		if (pos < server_state.win_base ||
				pos + size > server_state.win_base + server_state.win_size) {
			fprintf(stderr, "pos: 0x%08x size: 0x%08x aren't in active window\n",
					pos, size);
			fprintf(stderr, "window pos: 0x%08x window size: 0x%08x\n",
					server_state.win_base, server_state.win_size);
			return false;
		}
	} else {
		if (pos + size > server_state.win_base + server_state.win_size)
			return false;
	}
	return true;
}

/* skiboot test stubs */
int64_t lpc_read(enum OpalLPCAddressType __unused addr_type, uint32_t addr,
		 uint32_t *data, uint32_t sz);
int64_t lpc_read(enum OpalLPCAddressType __unused addr_type, uint32_t addr,
		 uint32_t *data, uint32_t sz)
{
	/* Let it read from a write window... Spec says it ok! */
	if (!check_window(addr, sz) || server_state.win_type == WIN_CLOSED)
		return 1;

	switch (sz) {
	case 1:
		*(uint8_t *)data = *(uint8_t *)(server_state.lpc_base + addr);
		break;
	case 2:
		*(uint16_t *)data = be16_to_cpu(*(uint16_t *)(server_state.lpc_base + addr));
		break;
	case 4:
		*(uint32_t *)data = be32_to_cpu(*(uint32_t *)(server_state.lpc_base + addr));
		break;
	default:
		prerror("Invalid data size %d\n", sz);
		return 1;
	}
	return 0;
}

int64_t lpc_write(enum OpalLPCAddressType __unused addr_type, uint32_t addr,
		  uint32_t data, uint32_t sz);
int64_t lpc_write(enum OpalLPCAddressType __unused addr_type, uint32_t addr,
		  uint32_t data, uint32_t sz)
{
	if (!check_window(addr, sz) || server_state.win_type != WIN_WRITE)
		return 1;
	switch (sz) {
	case 1:
		*(uint8_t *)(server_state.lpc_base + addr) = data;
		break;
	case 2:
		*(uint16_t *)(server_state.lpc_base + addr) = cpu_to_be16(data);
		break;
	case 4:
		*(uint32_t *)(server_state.lpc_base + addr) = cpu_to_be32(data);
		break;
	default:
		prerror("Invalid data size %d\n", sz);
		return 1;
	}
	return 0;
}

int64_t lpc_fw_read(uint32_t off, void *buf, uint32_t len);
int64_t lpc_fw_read(uint32_t off, void *buf, uint32_t len)
{
	int rc;

	while (len) {
		uint32_t chunk;
		uint32_t dat;

		/* XXX: make this read until it's aligned */
		if (len > 3 && !(off & 3)) {
			rc = lpc_read(OPAL_LPC_FW, off, &dat, 4);
			if (!rc) {
				/*
				 * lpc_read swaps to CPU endian but it's not
				 * really a 32-bit value, so convert back.
				 */
				*(__be32 *)buf = cpu_to_be32(dat);
			}
			chunk = 4;
		} else {
			rc = lpc_read(OPAL_LPC_FW, off, &dat, 1);
			if (!rc)
				*(uint8_t *)buf = dat;
			chunk = 1;
		}
		if (rc)
			return rc;

		len -= chunk;
		off += chunk;
		buf += chunk;
	}

	return 0;
}

int64_t lpc_fw_write(uint32_t off, const void *buf, uint32_t len);
int64_t lpc_fw_write(uint32_t off, const void *buf, uint32_t len)
{
	int rc;

	while (len) {
		uint32_t chunk;

		if (len > 3 && !(off & 3)) {
			/* endian swap: see lpc_window_write */
			uint32_t dat = be32_to_cpu(*(__be32 *)buf);

			rc = lpc_write(OPAL_LPC_FW, off, dat, 4);
			chunk = 4;
		} else {
			uint8_t dat = *(uint8_t *)buf;

			rc = lpc_write(OPAL_LPC_FW, off, dat, 1);
			chunk = 1;
		}
		if (rc)
			return rc;

		len -= chunk;
		off += chunk;
		buf += chunk;
	}

	return 0;
}

int bmc_mbox_register_attn(mbox_attn_cb handler, void *drv_data)
{
	mbox_data.attn = handler;
	mbox_data.cb_attn = drv_data;

	return 0;
}

uint8_t bmc_mbox_get_attn_reg(void)
{
	return server_state.attn_reg;
}

int bmc_mbox_register_callback(mbox_data_cb handler, void *drv_data)
{
	mbox_data.fn = handler;
	mbox_data.cb_data = drv_data;

	return 0;
}

static int close_window(bool check)
{
	/*
	 * This isn't strictly prohibited and some daemons let you close
	 * windows even if none are open.
	 * I've made the test fail because closing with no windows open is
	 * a sign that something 'interesting' has happened.
	 * You should investigate why
	 *
	 * If check is false it is because we just want to do the logic
	 * because open window has been called - you can open a window
	 * over a closed window obviously
	 */
	if (check && server_state.win_type == WIN_CLOSED)
		return MBOX_R_PARAM_ERROR;

	server_state.win_type = WIN_CLOSED;
	mprotect(server_state.lpc_base, server_state.lpc_size, PROT_NONE);

	return MBOX_R_SUCCESS;
}

static int do_dirty(uint32_t pos, uint32_t size)
{
	pos <<= server_state.block_shift;
	if (server_state.api > 1)
		size <<= server_state.block_shift;
	if (!check_window(pos, size)) {
		prlog(PR_ERR, "Trying to dirty not in open window range\n");
		return MBOX_R_PARAM_ERROR;
	}
	if (server_state.win_type != WIN_WRITE) {
		prlog(PR_ERR, "Trying to dirty not write window\n");
		return MBOX_R_PARAM_ERROR;
	}

	/* Thats about all actually */
	return MBOX_R_SUCCESS;
}

void check_timers(bool __unused unused)
{
	/* now that we've handled the message, holla-back */
	if (mbox_data.msg) {
		mbox_data.fn(mbox_data.msg, mbox_data.cb_data);
		mbox_data.msg = NULL;
	}
}

static int open_window(struct bmc_mbox_msg *msg, bool write, u32 offset, u32 size)
{
	int max_size = server_state.max_read_win << server_state.block_shift;
	//int win_size = server_state.def_read_win;
	enum win_type type = WIN_READ;
	int prot = PROT_READ;

	assert(server_state.win_type == WIN_CLOSED);

	/* Shift params up */
	offset <<= server_state.block_shift;
	size <<= server_state.block_shift;

	if (!size || server_state.api == 1)
		size = server_state.def_read_win << server_state.block_shift;

	if (write) {
		max_size = server_state.max_write_win << server_state.block_shift;
		//win_size = server_state.def_write_win;
		prot |= PROT_WRITE;
		type = WIN_WRITE;
		/* Use the default size if zero size is set */
		if (!size || server_state.api == 1)
			size = server_state.def_write_win << server_state.block_shift;
	}


	prlog(PR_INFO, "Opening range %#.8x, %#.8x for %s\n",
			offset, offset + size - 1, write ? "writing" : "reading");

	/* XXX: Document this behaviour */
	if ((size + offset) > server_state.lpc_size) {
		prlog(PR_INFO, "tried to open beyond end of flash\n");
		return MBOX_R_PARAM_ERROR;
	}

	/* XXX: should we do this before or after checking for errors?
	 * 	Doing it afterwards ensures consistency between
	 * 	implementations
	 */
	if (server_state.api == 2)
		size = MIN(size, max_size);

	mprotect(server_state.lpc_base + offset, size, prot);
	server_state.win_type = type;
	server_state.win_base = offset;
	server_state.win_size = size;

	memset(msg->args, 0, sizeof(msg->args));
	bmc_put_u16(msg, 0, offset >> server_state.block_shift);
	if (server_state.api == 1) {
		/*
		 * Put nonsense in here because v1 mbox-flash shouldn't know about it.
		 * If v1 mbox-flash does read this, 0xffff should trigger a big mistake.
		 */
		bmc_put_u16(msg, 2, 0xffff >> server_state.block_shift);
		bmc_put_u16(msg, 4, 0xffff >> server_state.block_shift);
	} else {
		bmc_put_u16(msg, 2, size >> server_state.block_shift);
		bmc_put_u16(msg, 4, offset >> server_state.block_shift);
	}
	return MBOX_R_SUCCESS;
}

int bmc_mbox_enqueue(struct bmc_mbox_msg *msg,
		unsigned int __unused timeout_sec)
{
	/*
	 * FIXME: should we be using the same storage for message
	 *        and response?
	 */
	int rc = MBOX_R_SUCCESS;
	uint32_t start, size;

	if (server_state.reset && msg->command != MBOX_C_GET_MBOX_INFO &&
				msg->command != MBOX_C_BMC_EVENT_ACK) {
		/*
		 * Real daemons should return an error, but for testing we'll
		 * be a bit more strict
		 */
		prlog(PR_EMERG, "Server was in reset state - illegal command %d\n",
			msg->command);
		exit(1);
	}

	switch (msg->command) {
		case MBOX_C_RESET_STATE:
			prlog(PR_INFO, "RESET_STATE\n");
			server_state.win_type = WIN_CLOSED;
			rc = open_window(msg, false, 0, LPC_BLOCKS);
			memset(msg->args, 0, sizeof(msg->args));
			break;

		case MBOX_C_GET_MBOX_INFO:
			prlog(PR_INFO, "GET_MBOX_INFO version = %d, block_shift = %d\n",
					server_state.api, server_state.block_shift);
			msg->args[0] = server_state.api;
			if (server_state.api == 1) {
				prlog(PR_INFO, "\tread_size = 0x%08x, write_size = 0x%08x\n",
						server_state.def_read_win, server_state.def_write_win);
				bmc_put_u16(msg, 1, server_state.def_read_win);
				bmc_put_u16(msg, 3, server_state.def_write_win);
				msg->args[5] = 0xff; /* If v1 reads this, 0xff will force the mistake */
			} else {
				msg->args[5] = server_state.block_shift;
			}
			server_state.reset = false;
			break;

		case MBOX_C_GET_FLASH_INFO:
			prlog(PR_INFO, "GET_FLASH_INFO: size: 0x%" PRIu64 ", erase: 0x%08x\n",
					server_state.lpc_size, server_state.erase_granule);
			if (server_state.api == 1) {
				bmc_put_u32(msg, 0, server_state.lpc_size);
				bmc_put_u32(msg, 4, server_state.erase_granule);
			} else {
				bmc_put_u16(msg, 0, server_state.lpc_size >> server_state.block_shift);
				bmc_put_u16(msg, 2, server_state.erase_granule >> server_state.block_shift);
			}
			break;

		case MBOX_C_CREATE_READ_WINDOW:
			start = bmc_get_u16(msg, 0);
			size = bmc_get_u16(msg, 2);
			prlog(PR_INFO, "CREATE_READ_WINDOW: pos: 0x%08x, len: 0x%08x\n", start, size);
			rc = close_window(false);
			if (rc != MBOX_R_SUCCESS)
				break;
			rc = open_window(msg, false, start, size);
			break;

		case MBOX_C_CLOSE_WINDOW:
			rc = close_window(true);
			break;

		case MBOX_C_CREATE_WRITE_WINDOW:
			start = bmc_get_u16(msg, 0);
			size = bmc_get_u16(msg, 2);
			prlog(PR_INFO, "CREATE_WRITE_WINDOW: pos: 0x%08x, len: 0x%08x\n", start, size);
			rc = close_window(false);
			if (rc != MBOX_R_SUCCESS)
				break;
			rc = open_window(msg, true, start, size);
			break;

		/* TODO: make these do something */
		case MBOX_C_WRITE_FLUSH:
			prlog(PR_INFO, "WRITE_FLUSH\n");
			/*
			 * This behaviour isn't strictly illegal however it could
			 * be a sign of bad behaviour
			 */
			if (server_state.api > 1 && !server_state.win_dirty) {
				prlog(PR_EMERG, "Version >1 called FLUSH without a previous DIRTY\n");
				exit (1);
			}
			server_state.win_dirty = false;
			if (server_state.api > 1)
				break;

			/* This is only done on V1 */
			start = bmc_get_u16(msg, 0);
			if (server_state.api == 1)
				size = bmc_get_u32(msg, 2);
			else
				size = bmc_get_u16(msg, 2);
			prlog(PR_INFO, "\tpos: 0x%08x len: 0x%08x\n", start, size);
			rc = do_dirty(start, size);
			break;
		case MBOX_C_MARK_WRITE_DIRTY:
			start = bmc_get_u16(msg, 0);
			if (server_state.api == 1)
				size = bmc_get_u32(msg, 2);
			else
				size = bmc_get_u16(msg, 2);
			prlog(PR_INFO, "MARK_WRITE_DIRTY: pos: 0x%08x, len: %08x\n", start, size);
			server_state.win_dirty = true;
			rc = do_dirty(start, size);
			break;
		case MBOX_C_BMC_EVENT_ACK:
			/*
			 * Clear any BMC notifier flags. Don't clear the server
			 * reset state here, it is a permitted command but only
			 * GET_INFO should clear it.
			 *
			 * Make sure that msg->args[0] is only acking bits we told
			 * it about, in server_state.attn_reg. The caveat is that
			 * it could NOT ack some bits...
			 */
			prlog(PR_INFO, "BMC_EVENT_ACK 0x%02x\n", msg->args[0]);
			if ((msg->args[0] | server_state.attn_reg) != server_state.attn_reg) {
				prlog(PR_EMERG, "Tried to ack bits we didn't say!\n");
				exit(1);
			}
			msg->bmc &= ~msg->args[0];
			server_state.attn_reg &= ~msg->args[0];
			break;
		case MBOX_C_MARK_WRITE_ERASED:
			start = bmc_get_u16(msg, 0) << server_state.block_shift;
			size = bmc_get_u16(msg, 2) << server_state.block_shift;
			/* If we've negotiated v1 this should never be called */
			if (server_state.api == 1) {
				prlog(PR_EMERG, "Version 1 protocol called a V2 only command\n");
				exit(1);
			}
			/*
			 * This will likely result in flush (but not
			 * dirty) being called. This is the point.
			 */
			server_state.win_dirty = true;
			/* This should really be done when they call flush */
			memset(server_state.lpc_base + server_state.win_base + start, 0xff, size);
			break;
		default:
			prlog(PR_EMERG, "Got unknown command code from mbox: %d\n", msg->command);
	}

	prerror("command response = %d\n", rc);
	msg->response = rc;

	mbox_data.msg = msg;

	return 0;
}

int mbox_server_memcmp(int off, const void *buf, size_t len)
{
	return memcmp(server_state.lpc_base + off, buf, len);
}

void mbox_server_memset(int c)
{
	memset(server_state.lpc_base, c, server_state.lpc_size);
}

uint32_t mbox_server_total_size(void)
{
	/* Not actually but for this server we don't differentiate */
	return server_state.lpc_size;
}

uint32_t mbox_server_erase_granule(void)
{
	return server_state.erase_granule;
}

int mbox_server_version(void)
{
	return server_state.api;
}

int mbox_server_reset(unsigned int version, uint8_t block_shift)
{
	if (version > 3)
		return 1;

	server_state.api = version;
	if (block_shift)
		server_state.block_shift = block_shift;
	if (server_state.erase_granule < (1 << server_state.block_shift))
		server_state.erase_granule = 1 << server_state.block_shift;
	server_state.lpc_size = LPC_BLOCKS * (1 << server_state.block_shift);
	free(server_state.lpc_base);
	server_state.lpc_base = malloc(server_state.lpc_size);
	server_state.attn_reg = MBOX_ATTN_BMC_REBOOT | MBOX_ATTN_BMC_DAEMON_READY;
	server_state.win_type = WIN_CLOSED;
	server_state.reset = true;
	mbox_data.attn(MBOX_ATTN_BMC_REBOOT, mbox_data.cb_attn);

	return 0;
}

int mbox_server_init(void)
{
	server_state.api = 1;
	server_state.reset = true;

	/* We're always ready! */
	server_state.attn_reg = MBOX_ATTN_BMC_DAEMON_READY;

	/* setup server */
	server_state.block_shift = 12;
	server_state.erase_granule = 0x1000;
	server_state.lpc_size = LPC_BLOCKS * (1 << server_state.block_shift);
	server_state.lpc_base = malloc(server_state.lpc_size);

	server_state.def_read_win = 1; /* These are in units of block shift "= 1 is 4K" */
	server_state.def_write_win = 1; /* These are in units of block shift "= 1 is 4K" */

	server_state.max_read_win = LPC_BLOCKS;
	server_state.max_write_win = LPC_BLOCKS;
	server_state.win_type = WIN_CLOSED;

	return 0;
}

void mbox_server_destroy(void)
{
	free(server_state.lpc_base);
}
