/* Copyright 2013-2014 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.
 */

#include <skiboot.h>
#include <fsp.h>
#include <opal.h>
#include <lock.h>
#include <device.h>
#include <errorlog.h>

/*
 * The FSP NVRAM API operates in "blocks" of 4K. It is entirely exposed
 * to the OS via the OPAL APIs.
 *
 * In order to avoid dealing with complicated read/modify/write state
 * machines (and added issues related to FSP failover in the middle)
 * we keep a memory copy of the entire nvram which we load at boot
 * time. We save only modified blocks.
 *
 * To limit the amount of memory used by the nvram image, we limit
 * how much nvram we support to NVRAM_SIZE. Additionally, this limit
 * of 1M is the maximum that the CHRP/PAPR nvram partition format
 * supports for a partition entry.
 *
 * (Q: should we save the whole thing in case of FSP failover ?)
 *
 * The nvram is expected to comply with the CHRP/PAPR defined format,
 * and specifically contain a System partition (ID 0x70) named "common"
 * with configuration variables for the bootloader and a FW private
 * partition for future use by skiboot.
 *
 * If the partition layout appears broken or lacks one of the above
 * partitions, we reformat the entire nvram at boot time.
 *
 * We do not exploit the ability of the FSP to store a checksum. This
 * is documented as possibly going away. The CHRP format for nvram
 * that Linux uses has its own (though weak) checksum mechanism already
 *
 */

#define NVRAM_BLKSIZE	0x1000

struct nvram_triplet {
	uint64_t	dma_addr;
	uint32_t	blk_offset;
	uint32_t	blk_count;
} __packed;

#define NVRAM_FLAG_CLEAR_WPEND	0x80000000

enum nvram_state {
	NVRAM_STATE_CLOSED,
	NVRAM_STATE_OPENING,
	NVRAM_STATE_BROKEN,
	NVRAM_STATE_OPEN,
	NVRAM_STATE_ABSENT,
};

static void *fsp_nvram_image;
static uint32_t fsp_nvram_size;
static struct lock fsp_nvram_lock = LOCK_UNLOCKED;
static struct fsp_msg *fsp_nvram_msg;
static uint32_t fsp_nvram_dirty_start;
static uint32_t fsp_nvram_dirty_end;
static bool fsp_nvram_was_read;
static struct nvram_triplet fsp_nvram_triplet __align(0x1000);
static enum nvram_state fsp_nvram_state = NVRAM_STATE_CLOSED;

DEFINE_LOG_ENTRY(OPAL_RC_NVRAM_INIT, OPAL_PLATFORM_ERR_EVT , OPAL_NVRAM,
		OPAL_MISC_SUBSYSTEM, OPAL_PREDICTIVE_ERR_GENERAL,
		OPAL_NA);

DEFINE_LOG_ENTRY(OPAL_RC_NVRAM_OPEN, OPAL_PLATFORM_ERR_EVT, OPAL_NVRAM,
		OPAL_MISC_SUBSYSTEM, OPAL_PREDICTIVE_ERR_GENERAL,
		OPAL_NA);

DEFINE_LOG_ENTRY(OPAL_RC_NVRAM_SIZE, OPAL_PLATFORM_ERR_EVT, OPAL_NVRAM,
		OPAL_MISC_SUBSYSTEM, OPAL_PREDICTIVE_ERR_GENERAL,
		OPAL_NA);

DEFINE_LOG_ENTRY(OPAL_RC_NVRAM_READ, OPAL_PLATFORM_ERR_EVT, OPAL_NVRAM,
		OPAL_MISC_SUBSYSTEM, OPAL_PREDICTIVE_ERR_GENERAL,
		OPAL_NA);

DEFINE_LOG_ENTRY(OPAL_RC_NVRAM_WRITE, OPAL_PLATFORM_ERR_EVT, OPAL_NVRAM,
		OPAL_MISC_SUBSYSTEM, OPAL_PREDICTIVE_ERR_GENERAL,
		OPAL_NA);

static void fsp_nvram_send_write(void);

static void fsp_nvram_wr_complete(struct fsp_msg *msg)
{
	struct fsp_msg *resp = msg->resp;
	uint8_t rc;

	lock(&fsp_nvram_lock);
	fsp_nvram_msg = NULL;

	/* Check for various errors. If an error occurred,
	 * we generally assume the nvram is completely dirty
	 * but we won't trigger a new write until we get
	 * either a new attempt at writing, or an FSP reset
	 * reload (TODO)
	 */
	if (!resp || resp->state != fsp_msg_response)
		goto fail_dirty;
	rc = (msg->word1 >> 8) & 0xff;
	switch(rc) {
	case 0:
	case 0x44:
		/* Sync to secondary required... XXX */
	case 0x45:
		break;
	case 0xef:
		/* Sync to secondary failed, let's ignore that for now,
		 * maybe when (if) we handle redundant FSPs ...
		 */
		prerror("FSP: NVRAM sync to secondary failed\n");
		break;
	default:
		log_simple_error(&e_info(OPAL_RC_NVRAM_WRITE),
			"FSP: NVRAM write return error 0x%02x\n", rc);
		goto fail_dirty;
	}
	fsp_freemsg(msg);
	if (fsp_nvram_dirty_start <= fsp_nvram_dirty_end)
		fsp_nvram_send_write();
	unlock(&fsp_nvram_lock);
	return;
 fail_dirty:
	fsp_nvram_dirty_start = 0;
	fsp_nvram_dirty_end = fsp_nvram_size - 1;
	fsp_freemsg(msg);
	unlock(&fsp_nvram_lock);
}

static void fsp_nvram_send_write(void)
{
	uint32_t start = fsp_nvram_dirty_start;
	uint32_t end = fsp_nvram_dirty_end;
	uint32_t count;

	if (start > end || fsp_nvram_state != NVRAM_STATE_OPEN)
		return;
	count = (end - start) / NVRAM_BLKSIZE + 1;
	fsp_nvram_triplet.dma_addr = PSI_DMA_NVRAM_BODY + start;
	fsp_nvram_triplet.blk_offset = start / NVRAM_BLKSIZE;
	fsp_nvram_triplet.blk_count = count;
	fsp_nvram_msg = fsp_mkmsg(FSP_CMD_WRITE_VNVRAM, 6,
				  0, PSI_DMA_NVRAM_TRIPL, 1,
				  NVRAM_FLAG_CLEAR_WPEND, 0, 0);
	if (fsp_queue_msg(fsp_nvram_msg, fsp_nvram_wr_complete)) {
		fsp_freemsg(fsp_nvram_msg);
		fsp_nvram_msg = NULL;
		log_simple_error(&e_info(OPAL_RC_NVRAM_WRITE),
				"FSP: Error queueing nvram update\n");
		return;
	}
	fsp_nvram_dirty_start = fsp_nvram_size;
	fsp_nvram_dirty_end = 0;
}

static void fsp_nvram_rd_complete(struct fsp_msg *msg)
{
	int64_t rc;

	lock(&fsp_nvram_lock);

	/* Read complete, check status. What to do if the read fails ?
	 *
	 * Well, there could be various reasons such as an FSP reboot
	 * at the wrong time, but there is really not much we can do
	 * so for now I'll just mark the nvram as closed, and we'll
	 * attempt a re-open and re-read whenever the OS tries to
	 * access it
	 */
	rc = (msg->resp->word1 >> 8) & 0xff;
	fsp_nvram_msg = NULL;
	fsp_freemsg(msg);
	if (rc) {
		prerror("FSP: NVRAM read failed, will try again later\n");
		fsp_nvram_state = NVRAM_STATE_CLOSED;
	} else {
		/* nvram was read once, no need to do it ever again */
		fsp_nvram_was_read = true;
		fsp_nvram_state = NVRAM_STATE_OPEN;

		/* XXX Here we should look for nvram settings that concern
		 * us such as guest kernel arguments etc...
		 */
	}
	unlock(&fsp_nvram_lock);
	nvram_read_complete(fsp_nvram_state == NVRAM_STATE_OPEN);
	if (fsp_nvram_state != NVRAM_STATE_OPEN)
		log_simple_error(&e_info(OPAL_RC_NVRAM_INIT),
		"FSP: NVRAM not read, skipping init\n");
}

static void fsp_nvram_send_read(void)
{
	fsp_nvram_msg = fsp_mkmsg(FSP_CMD_READ_VNVRAM, 4,
				  0, PSI_DMA_NVRAM_BODY, 0,
				  fsp_nvram_size / NVRAM_BLKSIZE);
	if (fsp_queue_msg(fsp_nvram_msg, fsp_nvram_rd_complete)) {
		/* If the nvram read fails to queue, we mark ourselves
		 * closed. Shouldn't have happened anyway. Not much else
		 * we can do.
		 */
		fsp_nvram_state = NVRAM_STATE_CLOSED;
		fsp_freemsg(fsp_nvram_msg);
		fsp_nvram_msg = NULL;
		log_simple_error(&e_info(OPAL_RC_NVRAM_READ),
				"FSP: Error queueing nvram read\n");
		return;
	}
}

static void fsp_nvram_open_complete(struct fsp_msg *msg)
{
	int8_t rc;

	lock(&fsp_nvram_lock);

	/* Open complete, check status */
	rc = (msg->resp->word1 >> 8) & 0xff;
	fsp_nvram_msg = NULL;
	fsp_freemsg(msg);
	if (rc) {
		log_simple_error(&e_info(OPAL_RC_NVRAM_OPEN),
			"FSP: NVRAM open failed, FSP error 0x%02x\n", rc);
		goto failed;
	}
	if (fsp_nvram_was_read)
		fsp_nvram_state = NVRAM_STATE_OPEN;
	else
		fsp_nvram_send_read();
	unlock(&fsp_nvram_lock);
	return;
 failed:
	fsp_nvram_state = NVRAM_STATE_CLOSED;
	unlock(&fsp_nvram_lock);
}

static void fsp_nvram_send_open(void)
{
	printf("FSP NVRAM: Opening nvram...\n");
	fsp_nvram_msg = fsp_mkmsg(FSP_CMD_OPEN_VNVRAM, 1, fsp_nvram_size);
	assert(fsp_nvram_msg);
	fsp_nvram_state = NVRAM_STATE_OPENING;
	if (!fsp_queue_msg(fsp_nvram_msg, fsp_nvram_open_complete))
		return;

	prerror("FSP NVRAM: Failed to queue nvram open message\n");
	fsp_freemsg(fsp_nvram_msg);
	fsp_nvram_msg = NULL;
	fsp_nvram_state = NVRAM_STATE_CLOSED;
}

static bool fsp_nvram_get_size(uint32_t *out_size)
{
	struct fsp_msg *msg;
	int rc, size;

	msg = fsp_mkmsg(FSP_CMD_GET_VNVRAM_SIZE, 0);
	assert(msg);

	rc = fsp_sync_msg(msg, false);
	size = msg->resp ? msg->resp->data.words[0] : 0;
	fsp_freemsg(msg);
	if (rc || size == 0) {
		log_simple_error(&e_info(OPAL_RC_NVRAM_SIZE),
			"FSP: Error %d nvram size reported is %d\n", rc, size);
		fsp_nvram_state = NVRAM_STATE_BROKEN;
		return false;
	}
	printf("FSP: NVRAM file size from FSP is %d bytes\n", size);
	*out_size = size;
	return true;
}

static bool fsp_nvram_msg_rr(u32 cmd_sub_mod, struct fsp_msg *msg)
{
	assert(msg == NULL);

	switch (cmd_sub_mod) {
	case FSP_RESET_START:
		printf("FSP: Closing NVRAM on account of FSP Reset\n");
		fsp_nvram_state = NVRAM_STATE_CLOSED;
		return true;
	case FSP_RELOAD_COMPLETE:
		printf("FSP: Reopening NVRAM of FSP Reload complete\n");
		lock(&fsp_nvram_lock);
		fsp_nvram_send_open();
		unlock(&fsp_nvram_lock);
		return true;
	}
	return false;
}

static struct fsp_client fsp_nvram_client_rr = {
	.message = fsp_nvram_msg_rr,
};

static bool fsp_vnvram_msg(u32 cmd_sub_mod, struct fsp_msg *msg)
{
	u32 cmd;
	struct fsp_msg *resp;

	assert(msg == NULL);
	switch (cmd_sub_mod) {
	case FSP_CMD_GET_VNV_STATS:
		prlog(PR_DEBUG,
		      "FSP NVRAM: Get vNVRAM statistics not supported\n");
		cmd = FSP_RSP_GET_VNV_STATS | FSP_STATUS_INVALID_SUBCMD;
		break;
	case FSP_CMD_FREE_VNV_STATS:
		prlog(PR_DEBUG,
		      "FSP NVRAM: Free vNVRAM statistics buffer not supported\n");
		cmd = FSP_RSP_FREE_VNV_STATS | FSP_STATUS_INVALID_SUBCMD;
		break;
	default:
		return false;
	}

	resp = fsp_mkmsg(cmd, 0);
	if (!resp) {
		prerror("FSP NVRAM: Failed to allocate resp message\n");
		return false;
	}
	if (fsp_queue_msg(resp, fsp_freemsg)) {
		prerror("FSP NVRAM: Failed to queue resp message\n");
		fsp_freemsg(resp);
		return false;
	}
	return true;
}

static struct fsp_client fsp_vnvram_client = {
	.message = fsp_vnvram_msg,
};

int fsp_nvram_info(uint32_t *total_size)
{
	if (!fsp_present()) {
		fsp_nvram_state = NVRAM_STATE_ABSENT;
		return OPAL_HARDWARE;
	}

	if (!fsp_nvram_get_size(total_size))
		return OPAL_HARDWARE;
	return OPAL_SUCCESS;
}

int fsp_nvram_start_read(void *dst, uint32_t src, uint32_t len)
{
	/* We are currently limited to fully aligned transfers */
	assert((((uint64_t)dst) & 0xfff) == 0);
	assert(dst);

	/* Currently don't support src!=0 */
	assert(src == 0);

	if (!fsp_present())
		return -ENODEV;

	op_display(OP_LOG, OP_MOD_INIT, 0x0007);

	lock(&fsp_nvram_lock);

	/* Store image info */
	fsp_nvram_image = dst;
	fsp_nvram_size = len;

	/* Mark nvram as not dirty */
	fsp_nvram_dirty_start = len;
	fsp_nvram_dirty_end = 0;

	/* Map TCEs */
	fsp_tce_map(PSI_DMA_NVRAM_TRIPL, &fsp_nvram_triplet,
		    PSI_DMA_NVRAM_TRIPL_SZ);
	fsp_tce_map(PSI_DMA_NVRAM_BODY, dst, PSI_DMA_NVRAM_BODY_SZ);

	/* Register for the reset/reload event */
	fsp_register_client(&fsp_nvram_client_rr, FSP_MCLASS_RR_EVENT);

	/* Register for virtual NVRAM interface events */
	fsp_register_client(&fsp_vnvram_client, FSP_MCLASS_VIRTUAL_NVRAM);

	/* Open and load the nvram from the FSP */
	fsp_nvram_send_open();

	unlock(&fsp_nvram_lock);

	return 0;
}

int fsp_nvram_write(uint32_t offset, void *src, uint32_t size)
{
	uint64_t end = offset + size - 1;

	/* We only support writing from the original image */
	if (src != fsp_nvram_image + offset)
		return OPAL_HARDWARE;

	offset &= ~(NVRAM_BLKSIZE - 1);
	end &= ~(NVRAM_BLKSIZE - 1);

	lock(&fsp_nvram_lock);
	/* If the nvram is closed, try re-opening */
	if (fsp_nvram_state == NVRAM_STATE_CLOSED)
		fsp_nvram_send_open();
	if (fsp_nvram_dirty_start > offset)
		fsp_nvram_dirty_start = offset;
	if (fsp_nvram_dirty_end < end)
		fsp_nvram_dirty_end = end;
	if (!fsp_nvram_msg && fsp_nvram_state == NVRAM_STATE_OPEN)
		fsp_nvram_send_write();
	unlock(&fsp_nvram_lock);

	return 0;
}
