/* Copyright 2018 IBM Corp.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#define pr_fmt(fmt) "HIOMAP: " fmt

#include <hiomap.h>
#include <inttypes.h>
#include <ipmi.h>
#include <lpc.h>
#include <mem_region-malloc.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>

#include <ccan/container_of/container_of.h>

#include "errors.h"
#include "ipmi-hiomap.h"

#define CMD_OP_HIOMAP_EVENT	0x0f

struct ipmi_hiomap_result {
	struct ipmi_hiomap *ctx;
	int16_t cc;
};

#define RESULT_INIT(_name, _ctx) struct ipmi_hiomap_result _name = { _ctx, -1 }

static inline uint32_t blocks_to_bytes(struct ipmi_hiomap *ctx, uint16_t blocks)
{
	return blocks << ctx->block_size_shift;
}

static inline uint16_t bytes_to_blocks(struct ipmi_hiomap *ctx, uint32_t bytes)
{
	return bytes >> ctx->block_size_shift;
}

static inline uint16_t bytes_to_blocks_align_up(struct ipmi_hiomap *ctx,
						uint32_t pos, uint32_t len)
{
	uint32_t block_size = 1 << ctx->block_size_shift;
	uint32_t delta = pos & (block_size - 1);
	uint32_t aligned = ALIGN_UP((len + delta), block_size);
	uint32_t blocks = aligned >> ctx->block_size_shift;
	/* Our protocol can handle block count < sizeof(u16) */
	uint32_t mask = ((1 << 16) - 1);

	assert(!(blocks & ~mask));

	return blocks & mask;
}

/* Call under ctx->lock */
static int hiomap_protocol_ready(struct ipmi_hiomap *ctx)
{
	if (!(ctx->bmc_state & HIOMAP_E_DAEMON_READY))
		return FLASH_ERR_DEVICE_GONE;
	if (ctx->bmc_state & HIOMAP_E_FLASH_LOST)
		return FLASH_ERR_AGAIN;

	return 0;
}

static int hiomap_queue_msg_sync(struct ipmi_hiomap *ctx, struct ipmi_msg *msg)
{
	int rc;

	/*
	 * There's an unavoidable TOCTOU race here with the BMC sending an
	 * event saying it's no-longer available right after we test but before
	 * we call into the IPMI stack to send the message.
	 * hiomap_queue_msg_sync() exists to capture the race in a single
	 * location.
	 */
	lock(&ctx->lock);
	rc = hiomap_protocol_ready(ctx);
	unlock(&ctx->lock);
	if (rc) {
		ipmi_free_msg(msg);
		return rc;
	}

	ipmi_queue_msg_sync(msg);

	return 0;
}

/* Call under ctx->lock */
static int hiomap_window_valid(struct ipmi_hiomap *ctx, uint64_t pos,
			        uint64_t len)
{
	if (ctx->bmc_state & HIOMAP_E_FLASH_LOST)
		return FLASH_ERR_AGAIN;
	if (ctx->bmc_state & HIOMAP_E_PROTOCOL_RESET)
		return FLASH_ERR_AGAIN;
	if (ctx->bmc_state & HIOMAP_E_WINDOW_RESET)
		return FLASH_ERR_AGAIN;
	if (ctx->window_state == closed_window)
		return FLASH_ERR_PARM_ERROR;
	if (pos < ctx->current.cur_pos)
		return FLASH_ERR_PARM_ERROR;
	if ((pos + len) > (ctx->current.cur_pos + ctx->current.size))
		return FLASH_ERR_PARM_ERROR;

	return 0;
}

static void ipmi_hiomap_cmd_cb(struct ipmi_msg *msg)
{
	struct ipmi_hiomap_result *res = msg->user_data;
	struct ipmi_hiomap *ctx = res->ctx;

	res->cc = msg->cc;
	if (msg->cc != IPMI_CC_NO_ERROR) {
		ipmi_free_msg(msg);
		return;
	}

	/* We at least need the command and sequence */
	if (msg->resp_size < 2) {
		prerror("Illegal response size: %u\n", msg->resp_size);
		res->cc = IPMI_ERR_UNSPECIFIED;
		ipmi_free_msg(msg);
		return;
	}

	if (msg->data[1] != ctx->seq) {
		prerror("Unmatched sequence number: wanted %u got %u\n",
			ctx->seq, msg->data[1]);
		res->cc = IPMI_ERR_UNSPECIFIED;
		ipmi_free_msg(msg);
		return;
	}

	switch (msg->data[0]) {
	case HIOMAP_C_GET_INFO:
	{
		struct hiomap_v2_info *parms;

		if (msg->resp_size != 6) {
			prerror("%u: Unexpected response size: %u\n", msg->data[0],
				msg->resp_size);
			res->cc = IPMI_ERR_UNSPECIFIED;
			break;
		}

		ctx->version = msg->data[2];
		if (ctx->version < 2) {
			prerror("Failed to negotiate protocol v2 or higher: %d\n",
				ctx->version);
			res->cc = IPMI_ERR_UNSPECIFIED;
			break;
		}

		parms = (struct hiomap_v2_info *)&msg->data[3];
		ctx->block_size_shift = parms->block_size_shift;
		ctx->timeout = le16_to_cpu(parms->timeout);
		break;
	}
	case HIOMAP_C_GET_FLASH_INFO:
	{
		struct hiomap_v2_flash_info *parms;

		if (msg->resp_size != 6) {
			prerror("%u: Unexpected response size: %u\n", msg->data[0],
				msg->resp_size);
			res->cc = IPMI_ERR_UNSPECIFIED;
			break;
		}

		parms = (struct hiomap_v2_flash_info *)&msg->data[2];
		ctx->total_size =
			blocks_to_bytes(ctx, le16_to_cpu(parms->total_size));
		ctx->erase_granule =
			blocks_to_bytes(ctx, le16_to_cpu(parms->erase_granule));
		break;
	}
	case HIOMAP_C_CREATE_READ_WINDOW:
	case HIOMAP_C_CREATE_WRITE_WINDOW:
	{
		struct hiomap_v2_create_window *parms;

		if (msg->resp_size != 8) {
			prerror("%u: Unexpected response size: %u\n", msg->data[0],
				msg->resp_size);
			res->cc = IPMI_ERR_UNSPECIFIED;
			break;
		}

		parms = (struct hiomap_v2_create_window *)&msg->data[2];

		ctx->current.lpc_addr =
			blocks_to_bytes(ctx, le16_to_cpu(parms->lpc_addr));
		ctx->current.size =
			blocks_to_bytes(ctx, le16_to_cpu(parms->size));
		ctx->current.cur_pos =
			blocks_to_bytes(ctx, le16_to_cpu(parms->offset));

		lock(&ctx->lock);
		if (msg->data[0] == HIOMAP_C_CREATE_READ_WINDOW)
			ctx->window_state = read_window;
		else
			ctx->window_state = write_window;
		unlock(&ctx->lock);

		break;
	}
	case HIOMAP_C_MARK_DIRTY:
	case HIOMAP_C_FLUSH:
	case HIOMAP_C_ACK:
	case HIOMAP_C_ERASE:
		if (msg->resp_size != 2) {
			prerror("%u: Unexpected response size: %u\n", msg->data[0],
				msg->resp_size);
			res->cc = IPMI_ERR_UNSPECIFIED;
			break;
		}
		break;
	default:
		prlog(PR_WARNING, "Unimplemented command handler: %u\n",
		      msg->data[0]);
		break;
	};
	ipmi_free_msg(msg);
}

static void hiomap_init(struct ipmi_hiomap *ctx)
{
	/*
	 * Speculatively mark the daemon as available so we attempt to perform
	 * the handshake without immediately bailing out.
	 */
	lock(&ctx->lock);
	ctx->bmc_state = HIOMAP_E_DAEMON_READY;
	unlock(&ctx->lock);
}

static int hiomap_get_info(struct ipmi_hiomap *ctx)
{
	RESULT_INIT(res, ctx);
	unsigned char req[3];
	struct ipmi_msg *msg;
	int rc;

	/* Negotiate protocol version 2 */
	req[0] = HIOMAP_C_GET_INFO;
	req[1] = ++ctx->seq;
	req[2] = HIOMAP_V2;

	msg = ipmi_mkmsg(IPMI_DEFAULT_INTERFACE,
		         bmc_platform->sw->ipmi_oem_hiomap_cmd,
			 ipmi_hiomap_cmd_cb, &res, req, sizeof(req), 6);

	rc = hiomap_queue_msg_sync(ctx, msg);
	if (rc)
		return rc;

	if (res.cc != IPMI_CC_NO_ERROR) {
		prerror("%s failed: %d\n", __func__, res.cc);
		return FLASH_ERR_PARM_ERROR; /* XXX: Find something better? */
	}

	return 0;
}

static int hiomap_get_flash_info(struct ipmi_hiomap *ctx)
{
	RESULT_INIT(res, ctx);
	unsigned char req[2];
	struct ipmi_msg *msg;
	int rc;

	req[0] = HIOMAP_C_GET_FLASH_INFO;
	req[1] = ++ctx->seq;
	msg = ipmi_mkmsg(IPMI_DEFAULT_INTERFACE,
		         bmc_platform->sw->ipmi_oem_hiomap_cmd,
			 ipmi_hiomap_cmd_cb, &res, req, sizeof(req), 2 + 2 + 2);

	rc = hiomap_queue_msg_sync(ctx, msg);
	if (rc)
		return rc;

	if (res.cc != IPMI_CC_NO_ERROR) {
		prerror("%s failed: %d\n", __func__, res.cc);
		return FLASH_ERR_PARM_ERROR; /* XXX: Find something better? */
	}

	return 0;
}

static int hiomap_window_move(struct ipmi_hiomap *ctx, uint8_t command,
			      uint64_t pos, uint64_t len, uint64_t *size)
{
	enum lpc_window_state want_state;
	struct hiomap_v2_range *range;
	RESULT_INIT(res, ctx);
	unsigned char req[6];
	struct ipmi_msg *msg;
	bool valid_state;
	bool is_read;
	int rc;

	is_read = (command == HIOMAP_C_CREATE_READ_WINDOW);
	want_state = is_read ? read_window : write_window;

	lock(&ctx->lock);

	valid_state = want_state == ctx->window_state;
	rc = hiomap_window_valid(ctx, pos, len);
	if (valid_state && !rc) {
		unlock(&ctx->lock);
		*size = len;
		return 0;
	}

	ctx->window_state = closed_window;

	unlock(&ctx->lock);

	req[0] = command;
	req[1] = ++ctx->seq;

	range = (struct hiomap_v2_range *)&req[2];
	range->offset = cpu_to_le16(bytes_to_blocks(ctx, pos));
	range->size = cpu_to_le16(bytes_to_blocks_align_up(ctx, pos, len));

	msg = ipmi_mkmsg(IPMI_DEFAULT_INTERFACE,
		         bmc_platform->sw->ipmi_oem_hiomap_cmd,
			 ipmi_hiomap_cmd_cb, &res, req, sizeof(req),
			 2 + 2 + 2 + 2);

	rc = hiomap_queue_msg_sync(ctx, msg);
	if (rc)
		return rc;

	if (res.cc != IPMI_CC_NO_ERROR) {
		prlog(PR_INFO, "%s failed: %d\n", __func__, res.cc);
		return FLASH_ERR_PARM_ERROR; /* XXX: Find something better? */
	}

	lock(&ctx->lock);
	*size = len;
	/* Is length past the end of the window? */
	if ((pos + len) > (ctx->current.cur_pos + ctx->current.size))
		/* Adjust size to meet current window */
		*size = (ctx->current.cur_pos + ctx->current.size) - pos;

	if (len != 0 && *size == 0) {
		unlock(&ctx->lock);
		prerror("Invalid window properties: len: %"PRIu64", size: %"PRIu64"\n",
			len, *size);
		return FLASH_ERR_PARM_ERROR;
	}

	prlog(PR_DEBUG, "Opened %s window from 0x%x for %u bytes at 0x%x\n",
	      (command == HIOMAP_C_CREATE_READ_WINDOW) ? "read" : "write",
	      ctx->current.cur_pos, ctx->current.size, ctx->current.lpc_addr);

	unlock(&ctx->lock);

	return 0;
}

static int hiomap_mark_dirty(struct ipmi_hiomap *ctx, uint64_t offset,
			      uint64_t size)
{
	struct hiomap_v2_range *range;
	enum lpc_window_state state;
	RESULT_INIT(res, ctx);
	unsigned char req[6];
	struct ipmi_msg *msg;
	uint32_t pos;
	int rc;

	lock(&ctx->lock);
	state = ctx->window_state;
	unlock(&ctx->lock);

	if (state != write_window)
		return FLASH_ERR_PARM_ERROR;

	req[0] = HIOMAP_C_MARK_DIRTY;
	req[1] = ++ctx->seq;

	pos = offset - ctx->current.cur_pos;
	range = (struct hiomap_v2_range *)&req[2];
	range->offset = cpu_to_le16(bytes_to_blocks(ctx, pos));
	range->size = cpu_to_le16(bytes_to_blocks_align_up(ctx, pos, size));

	msg = ipmi_mkmsg(IPMI_DEFAULT_INTERFACE,
		         bmc_platform->sw->ipmi_oem_hiomap_cmd,
			 ipmi_hiomap_cmd_cb, &res, req, sizeof(req), 2);

	rc = hiomap_queue_msg_sync(ctx, msg);
	if (rc)
		return rc;

	if (res.cc != IPMI_CC_NO_ERROR) {
		prerror("%s failed: %d\n", __func__, res.cc);
		return FLASH_ERR_PARM_ERROR;
	}

	prlog(PR_DEBUG, "Marked flash dirty at 0x%" PRIx64 " for %" PRIu64 "\n",
	      offset, size);

	return 0;
}

static int hiomap_flush(struct ipmi_hiomap *ctx)
{
	enum lpc_window_state state;
	RESULT_INIT(res, ctx);
	unsigned char req[2];
	struct ipmi_msg *msg;
	int rc;

	lock(&ctx->lock);
	state = ctx->window_state;
	unlock(&ctx->lock);

	if (state != write_window)
		return FLASH_ERR_PARM_ERROR;

	req[0] = HIOMAP_C_FLUSH;
	req[1] = ++ctx->seq;

	msg = ipmi_mkmsg(IPMI_DEFAULT_INTERFACE,
		         bmc_platform->sw->ipmi_oem_hiomap_cmd,
			 ipmi_hiomap_cmd_cb, &res, req, sizeof(req), 2);

	rc = hiomap_queue_msg_sync(ctx, msg);
	if (rc)
		return rc;

	if (res.cc != IPMI_CC_NO_ERROR) {
		prerror("%s failed: %d\n", __func__, res.cc);
		return FLASH_ERR_PARM_ERROR;
	}

	prlog(PR_DEBUG, "Flushed writes\n");

	return 0;
}

static int hiomap_ack(struct ipmi_hiomap *ctx, uint8_t ack)
{
	RESULT_INIT(res, ctx);
	unsigned char req[3];
	struct ipmi_msg *msg;
	int rc;

	req[0] = HIOMAP_C_ACK;
	req[1] = ++ctx->seq;
	req[2] = ack;

	msg = ipmi_mkmsg(IPMI_DEFAULT_INTERFACE,
		         bmc_platform->sw->ipmi_oem_hiomap_cmd,
			 ipmi_hiomap_cmd_cb, &res, req, sizeof(req), 2);

	rc = hiomap_queue_msg_sync(ctx, msg);
	if (rc)
		return rc;

	if (res.cc != IPMI_CC_NO_ERROR) {
		prlog(PR_DEBUG, "%s failed: %d\n", __func__, res.cc);
		return FLASH_ERR_PARM_ERROR;
	}

	prlog(PR_DEBUG, "Acked events: 0x%x\n", ack);

	return 0;
}

static int hiomap_erase(struct ipmi_hiomap *ctx, uint64_t offset,
			 uint64_t size)
{
	struct hiomap_v2_range *range;
	enum lpc_window_state state;
	RESULT_INIT(res, ctx);
	unsigned char req[6];
	struct ipmi_msg *msg;
	uint32_t pos;
	int rc;

	lock(&ctx->lock);
	state = ctx->window_state;
	unlock(&ctx->lock);

	if (state != write_window)
		return FLASH_ERR_PARM_ERROR;

	req[0] = HIOMAP_C_ERASE;
	req[1] = ++ctx->seq;

	pos = offset - ctx->current.cur_pos;
	range = (struct hiomap_v2_range *)&req[2];
	range->offset = cpu_to_le16(bytes_to_blocks(ctx, pos));
	range->size = cpu_to_le16(bytes_to_blocks_align_up(ctx, pos, size));

	msg = ipmi_mkmsg(IPMI_DEFAULT_INTERFACE,
		         bmc_platform->sw->ipmi_oem_hiomap_cmd,
			 ipmi_hiomap_cmd_cb, &res, req, sizeof(req), 2);
	rc = hiomap_queue_msg_sync(ctx, msg);
	if (rc)
		return rc;

	if (res.cc != IPMI_CC_NO_ERROR) {
		prerror("%s failed: %d\n", __func__, res.cc);
		return FLASH_ERR_PARM_ERROR;
	}

	prlog(PR_DEBUG, "Erased flash at 0x%" PRIx64 " for %" PRIu64 "\n",
	      offset, size);

	return 0;
}

static void hiomap_event(uint8_t events, void *context)
{
	struct ipmi_hiomap *ctx = context;

	prlog(PR_DEBUG, "Received events: 0x%x\n", events);

	lock(&ctx->lock);
	ctx->bmc_state = events | (ctx->bmc_state & HIOMAP_E_ACK_MASK);
	unlock(&ctx->lock);
}

static int lpc_window_read(struct ipmi_hiomap *ctx, uint32_t pos,
			   void *buf, uint32_t len)
{
	uint32_t off = ctx->current.lpc_addr + (pos - ctx->current.cur_pos);
	int rc;

	if ((ctx->current.lpc_addr + ctx->current.size) < (off + len))
		return FLASH_ERR_PARM_ERROR;

	prlog(PR_TRACE, "Reading at 0x%08x for 0x%08x offset: 0x%08x\n",
	      pos, len, off);

	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)
				*(uint32_t *)buf = dat;
			chunk = 4;
		} else {
			rc = lpc_read(OPAL_LPC_FW, off, &dat, 1);
			if (!rc)
				*(uint8_t *)buf = dat;
			chunk = 1;
		}
		if (rc) {
			prlog(PR_ERR, "lpc_read failure %d to FW 0x%08x\n", rc, off);
			return rc;
		}
		len -= chunk;
		off += chunk;
		buf += chunk;
	}

	return 0;
}

static int lpc_window_write(struct ipmi_hiomap *ctx, uint32_t pos,
			    const void *buf, uint32_t len)
{
	uint32_t off = ctx->current.lpc_addr + (pos - ctx->current.cur_pos);
	enum lpc_window_state state;
	int rc;

	lock(&ctx->lock);
	state = ctx->window_state;
	unlock(&ctx->lock);

	if (state != write_window)
		return FLASH_ERR_PARM_ERROR;

	if ((ctx->current.lpc_addr + ctx->current.size) < (off + len))
		return FLASH_ERR_PARM_ERROR;

	prlog(PR_TRACE, "Writing at 0x%08x for 0x%08x offset: 0x%08x\n",
	      pos, len, off);

	while(len) {
		uint32_t chunk;

		if (len > 3 && !(off & 3)) {
			rc = lpc_write(OPAL_LPC_FW, off,
				       *(uint32_t *)buf, 4);
			chunk = 4;
		} else {
			rc = lpc_write(OPAL_LPC_FW, off,
				       *(uint8_t *)buf, 1);
			chunk = 1;
		}
		if (rc) {
			prlog(PR_ERR, "lpc_write failure %d to FW 0x%08x\n", rc, off);
			return rc;
		}
		len -= chunk;
		off += chunk;
		buf += chunk;
	}

	return 0;
}

/* Best-effort asynchronous event handling by blocklevel callbacks */
static int ipmi_hiomap_handle_events(struct ipmi_hiomap *ctx)
{
	uint8_t status;
	int rc;

	lock(&ctx->lock);

	status = ctx->bmc_state;

	/*
	 * Immediately clear the ackable events to make sure we don't race to
	 * clear them after dropping the lock, as we may lose protocol or
	 * window state if a race materialises. In the event of a failure where
	 * we haven't completed the recovery, the state we mask out below gets
	 * OR'ed back in to avoid losing it.
	 */
	ctx->bmc_state &= ~HIOMAP_E_ACK_MASK;

	/*
	 * We won't be attempting to restore window state -
	 * ipmi_hiomap_handle_events() is followed by hiomap_window_move() in
	 * all cases. Attempting restoration after HIOMAP_E_PROTOCOL_RESET or
	 * HIOMAP_E_WINDOW_RESET can be wasteful if we immediately shift the
	 * window elsewhere, and if it does not need to be shifted with respect
	 * to the subsequent request then hiomap_window_move() will handle
	 * re-opening it from the closed state.
	 *
	 * Therefore it is enough to mark the window as closed to consider it
	 * recovered.
	 */
	if (status & (HIOMAP_E_PROTOCOL_RESET | HIOMAP_E_WINDOW_RESET))
		ctx->window_state = closed_window;

	unlock(&ctx->lock);

	/*
	 * If there's anything to acknowledge, do so in the one request to
	 * minimise overhead. By sending the ACK prior to performing the
	 * protocol recovery we ensure that even with coalesced resets we still
	 * end up in the recovered state and not unknowingly stuck in a reset
	 * state. We may receive reset events after the ACK but prior to the
	 * recovery procedures being run, but this just means that we will
	 * needlessly perform recovery on the following invocation of
	 * ipmi_hiomap_handle_events(). If the reset event is a
	 * HIOMAP_E_WINDOW_RESET it is enough that the window is already marked
	 * as closed above - future accesses will force it to be re-opened and
	 * the BMC's cache must be valid if opening the window is successful.
	 */
	if (status & HIOMAP_E_ACK_MASK) {
		/* ACK is unversioned, can send it if the daemon is ready */
		rc = hiomap_ack(ctx, status & HIOMAP_E_ACK_MASK);
		if (rc) {
			prlog(PR_DEBUG, "Failed to ack events: 0x%x\n",
			      status & HIOMAP_E_ACK_MASK);
			goto restore;
		}
	}

	if (status & HIOMAP_E_PROTOCOL_RESET) {
		prlog(PR_INFO, "Protocol was reset\n");

		rc = hiomap_get_info(ctx);
		if (rc) {
			prerror("Failure to renegotiate after protocol reset\n");
			goto restore;
		}

		rc = hiomap_get_flash_info(ctx);
		if (rc) {
			prerror("Failure to fetch flash info after protocol reset\n");
			goto restore;
		}

		prlog(PR_INFO, "Restored state after protocol reset\n");
	}

	/*
	 * As there's no change to the protocol on HIOMAP_E_WINDOW_RESET we
	 * simply need to open a window to recover, which as mentioned above is
	 * handled by hiomap_window_move() after our cleanup here.
	 */

	return 0;

restore:
	/*
	 * Conservatively restore the events to the un-acked state to avoid
	 * losing events due to races. It might cause us to restore state more
	 * than necessary, but never less than necessary.
	 */
	lock(&ctx->lock);
	ctx->bmc_state |= (status & HIOMAP_E_ACK_MASK);
	unlock(&ctx->lock);

	return rc;
}

static int ipmi_hiomap_read(struct blocklevel_device *bl, uint64_t pos,
			    void *buf, uint64_t len)
{
	struct ipmi_hiomap *ctx;
	uint64_t size;
	int rc = 0;

	/* LPC is only 32bit */
	if (pos > UINT_MAX || len > UINT_MAX)
		return FLASH_ERR_PARM_ERROR;

	ctx = container_of(bl, struct ipmi_hiomap, bl);

	rc = ipmi_hiomap_handle_events(ctx);
	if (rc)
		return rc;

	prlog(PR_TRACE, "Flash read at %#" PRIx64 " for %#" PRIx64 "\n", pos,
	      len);
	while (len > 0) {
		/* Move window and get a new size to read */
		rc = hiomap_window_move(ctx, HIOMAP_C_CREATE_READ_WINDOW, pos,
				        len, &size);
		if (rc)
			return rc;

		/* Perform the read for this window */
		rc = lpc_window_read(ctx, pos, buf, size);
		if (rc)
			return rc;

		/* Check we can trust what we read */
		lock(&ctx->lock);
		rc = hiomap_window_valid(ctx, pos, size);
		unlock(&ctx->lock);
		if (rc)
			return rc;

		len -= size;
		pos += size;
		buf += size;
	}
	return rc;

}

static int ipmi_hiomap_write(struct blocklevel_device *bl, uint64_t pos,
			     const void *buf, uint64_t len)
{
	struct ipmi_hiomap *ctx;
	uint64_t size;
	int rc = 0;

	/* LPC is only 32bit */
	if (pos > UINT_MAX || len > UINT_MAX)
		return FLASH_ERR_PARM_ERROR;

	ctx = container_of(bl, struct ipmi_hiomap, bl);

	rc = ipmi_hiomap_handle_events(ctx);
	if (rc)
		return rc;

	prlog(PR_TRACE, "Flash write at %#" PRIx64 " for %#" PRIx64 "\n", pos,
	      len);
	while (len > 0) {
		/* Move window and get a new size to read */
		rc = hiomap_window_move(ctx, HIOMAP_C_CREATE_WRITE_WINDOW, pos,
				        len, &size);
		if (rc)
			return rc;

		/* Perform the write for this window */
		rc = lpc_window_write(ctx, pos, buf, size);
		if (rc)
			return rc;

		rc = hiomap_mark_dirty(ctx, pos, size);
		if (rc)
			return rc;

		/*
		 * The BMC *should* flush if the window is implicitly closed,
		 * but do an explicit flush here to be sure.
		 *
		 * XXX: Removing this could improve performance
		 */
		rc = hiomap_flush(ctx);
		if (rc)
			return rc;

		len -= size;
		pos += size;
		buf += size;
	}
	return rc;
}

static int ipmi_hiomap_erase(struct blocklevel_device *bl, uint64_t pos,
			     uint64_t len)
{
	struct ipmi_hiomap *ctx;
	int rc;

	/* LPC is only 32bit */
	if (pos > UINT_MAX || len > UINT_MAX)
		return FLASH_ERR_PARM_ERROR;

	ctx = container_of(bl, struct ipmi_hiomap, bl);

	rc = ipmi_hiomap_handle_events(ctx);
	if (rc)
		return rc;

	prlog(PR_TRACE, "Flash erase at 0x%08x for 0x%08x\n", (u32) pos,
	      (u32) len);
	while (len > 0) {
		uint64_t size;

		/* Move window and get a new size to erase */
		rc = hiomap_window_move(ctx, HIOMAP_C_CREATE_WRITE_WINDOW, pos,
				        len, &size);
		if (rc)
			return rc;

		rc = hiomap_erase(ctx, pos, size);
		if (rc)
			return rc;

		/*
		 * Flush directly, don't mark that region dirty otherwise it
		 * isn't clear if a write happened there or not
		 */
		rc = hiomap_flush(ctx);
		if (rc)
			return rc;

		len -= size;
		pos += size;
	}

	return 0;
}

static int ipmi_hiomap_get_flash_info(struct blocklevel_device *bl,
				      const char **name, uint64_t *total_size,
				      uint32_t *erase_granule)
{
	struct ipmi_hiomap *ctx;
	int rc;

	ctx = container_of(bl, struct ipmi_hiomap, bl);

	rc = ipmi_hiomap_handle_events(ctx);
	if (rc)
		return rc;

	rc = hiomap_get_flash_info(ctx);
	if (rc)
		return rc;

	ctx->bl.erase_mask = ctx->erase_granule - 1;

	if (name)
		*name = NULL;
	if (total_size)
		*total_size = ctx->total_size;
	if (erase_granule)
		*erase_granule = ctx->erase_granule;

	return 0;
}

int ipmi_hiomap_init(struct blocklevel_device **bl)
{
	struct ipmi_hiomap *ctx;
	int rc;

	if (!bmc_platform->sw->ipmi_oem_hiomap_cmd)
		/* FIXME: Find a better error code */
		return FLASH_ERR_DEVICE_GONE;

	if (!bl)
		return FLASH_ERR_PARM_ERROR;

	*bl = NULL;

	ctx = zalloc(sizeof(struct ipmi_hiomap));
	if (!ctx)
		return FLASH_ERR_MALLOC_FAILED;

	init_lock(&ctx->lock);

	ctx->bl.read = &ipmi_hiomap_read;
	ctx->bl.write = &ipmi_hiomap_write;
	ctx->bl.erase = &ipmi_hiomap_erase;
	ctx->bl.get_info = &ipmi_hiomap_get_flash_info;

	hiomap_init(ctx);

	/* Ack all pending ack-able events to avoid spurious failures */
	rc = hiomap_ack(ctx, HIOMAP_E_ACK_MASK);
	if (rc) {
		prlog(PR_DEBUG, "Failed to ack events: 0x%x\n",
		      HIOMAP_E_ACK_MASK);
		goto err;
	}

	rc = ipmi_sel_register(CMD_OP_HIOMAP_EVENT, hiomap_event, ctx);
	if (rc < 0)
		goto err;

	/* Negotiate protocol behaviour */
	rc = hiomap_get_info(ctx);
	if (rc) {
		prerror("Failed to get hiomap parameters: %d\n", rc);
		goto err;
	}

	/* Grab the flash parameters */
	rc = hiomap_get_flash_info(ctx);
	if (rc) {
		prerror("Failed to get flash parameters: %d\n", rc);
		goto err;
	}

	prlog(PR_NOTICE, "Negotiated hiomap protocol v%u\n", ctx->version);
	prlog(PR_NOTICE, "Block size is %uKiB\n",
	      1 << (ctx->block_size_shift - 10));
	prlog(PR_NOTICE, "BMC suggested flash timeout of %us\n", ctx->timeout);
	prlog(PR_NOTICE, "Flash size is %uMiB\n", ctx->total_size >> 20);
	prlog(PR_NOTICE, "Erase granule size is %uKiB\n",
	      ctx->erase_granule >> 10);

	ctx->bl.keep_alive = 0;

	*bl = &(ctx->bl);

	return 0;

err:
	free(ctx);

	return rc;
}

void ipmi_hiomap_exit(struct blocklevel_device *bl)
{
	struct ipmi_hiomap *ctx;
	if (bl) {
		ctx = container_of(bl, struct ipmi_hiomap, bl);
		free(ctx);
	}
}
