/* 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 <stdlib.h>
#include <string.h>
#include <ipmi.h>
#include <lock.h>
#include <opal.h>
#include <device.h>

struct product_info {
	char *manufacturer;
	char *product;
	char *part_no;
	char *version;
	char *serial_no;
	char *asset_tag;
};

struct common_header {
	u8 version;
	u8 internal_offset;
	u8 chassis_offset;
	u8 board_offset;
	u8 product_offset;
	u8 multirecord_offset;
	u8 pad;
	u8 checksum;
} __packed;

#define min(x,y) ((x) < (y) ? x : y)

/* The maximum amount of FRU data we can store. */
#define FRU_DATA_SIZE 256

/* We allocate two bytes at these locations in the data array to track
 * state. */
#define WRITE_INDEX 256
#define REMAINING 257

/* The ASCII string encoding used only has 5 bits to encode length
 * hence the maximum is 31 characters. */
#define MAX_STR_LEN 31

static u8 fru_dev_id = 0;

static int fru_insert_string(u8 *buf, char *str)
{
	int len = strlen(str);

	/* The ASCII type/length format only supports a string length
	 * between 2 and 31 characters. Zero characters is ok though
	 * as it indicates no data present. */
	if (len == 1 || len > MAX_STR_LEN)
		return OPAL_PARAMETER;

	buf[0] = 0xc0 | len;
	memcpy(&buf[1], str, len);

	return len + 1;
}

static u8 fru_checksum(u8 *buf, int len)
{
	int i;
	u8 checksum = 0;

	for(i = 0; i < len; i++) {
		checksum += buf[i];
	}
	checksum = ~checksum + 1;
	return checksum;
}

#define FRU_INSERT_STRING(x, y)						\
	({ rc = fru_insert_string(x, y);				\
		if (rc < 1) return OPAL_PARAMETER; rc; })

static int fru_fill_product_info(u8 *buf, struct product_info *info, size_t size)
{
	size_t total_size = 11;
	int index = 0;
	int rc;

	total_size += strlen(info->manufacturer);
	total_size += strlen(info->product);
	total_size += strlen(info->part_no);
	total_size += strlen(info->version);
	total_size += strlen(info->serial_no);
	total_size += strlen(info->asset_tag);
	total_size += (8 - (total_size % 8)) % 8;
	if (total_size > size)
		return OPAL_PARAMETER;

	buf[index++] = 0x1;		/* Version */
	buf[index++] = total_size / 8;	/* Size */
	buf[index++] = 0;		/* Language code (English) */

	index += FRU_INSERT_STRING(&buf[index], info->manufacturer);
	index += FRU_INSERT_STRING(&buf[index], info->product);
	index += FRU_INSERT_STRING(&buf[index], info->part_no);
	index += FRU_INSERT_STRING(&buf[index], info->version);
	index += FRU_INSERT_STRING(&buf[index], info->serial_no);
	index += FRU_INSERT_STRING(&buf[index], info->asset_tag);

	buf[index++] = 0xc1;		/* End of data marker */
	memset(&buf[index], 0, total_size - index - 1);
	index += total_size - index - 1;
	buf[index] = fru_checksum(buf, index);
	assert(index == total_size - 1);

	return total_size;
}

static int fru_add(u8 *buf, int size)
{
	int len;
	char short_version[MAX_STR_LEN + 1];
	struct common_header common_hdr;
	struct product_info info = {
		.manufacturer = (char *) "IBM",
		.product = (char *) "skiboot",
		.part_no = (char *) "",
		.serial_no = (char *) "",
		.asset_tag = (char *) "",
	};

	if (size < sizeof(common_hdr))
		return OPAL_PARAMETER;

	/* We currently only support adding the version number at the
	 * product information offset. We choose an offset of 64 bytes
	 * because that's what the standard recommends. */
	common_hdr.version = 1;
	common_hdr.internal_offset = 0;
	common_hdr.chassis_offset = 0;
	common_hdr.board_offset = 0;
	common_hdr.product_offset = 64/8;
	common_hdr.multirecord_offset = 0;
	common_hdr.pad = 0;
	common_hdr.checksum = fru_checksum((u8 *) &common_hdr, sizeof(common_hdr) - 1);
	memcpy(buf, &common_hdr, sizeof(common_hdr));

	info.version = short_version;
	if (!strncmp(version, "skiboot-", 8))
		strncpy(info.version, &version[8], MAX_STR_LEN + 1);
	else
		strncpy(info.version, version, MAX_STR_LEN + 1);

	if (info.version[MAX_STR_LEN] != '\0')
		info.version[MAX_STR_LEN - 1] = '+';
	info.version[MAX_STR_LEN] = '\0';

	len = fru_fill_product_info(&buf[64], &info, size - 64);
	if (len < 0)
		return OPAL_PARAMETER;

	return len + 64;
}

static void fru_write_complete(struct ipmi_msg *msg)
{
	u8 write_count = msg->data[0];
	u16 offset;

	msg->data[WRITE_INDEX] += write_count;
	msg->data[REMAINING] -= write_count;
	if (msg->data[REMAINING] == 0)
		goto out;

	offset = msg->data[WRITE_INDEX];
	ipmi_init_msg(msg, IPMI_DEFAULT_INTERFACE, IPMI_WRITE_FRU,
		      fru_write_complete, NULL,
		      MIN(msg->data[REMAINING] + 3, IPMI_MAX_REQ_SIZE), 2);

	memmove(&msg->data[3], &msg->data[offset + 3], msg->req_size - 3);

	msg->data[0] = fru_dev_id;     		/* FRU Device ID */
	msg->data[1] = offset & 0xff;		/* Offset LSB */
	msg->data[2] = (offset >> 8) & 0xff;	/* Offset MSB */

	ipmi_queue_msg(msg);

	return;

out:
	ipmi_free_msg(msg);
}

static int fru_write(void)
{
	struct ipmi_msg *msg;
	int len;

	/* We allocate FRU_DATA_SIZE + 5 bytes for the message:
	 * - 3 bytes for the the write FRU command header
	 * - FRU_DATA_SIZE bytes for FRU data
	 * - 2 bytes for offset & bytes remaining count
	 */
	msg = ipmi_mkmsg(IPMI_DEFAULT_INTERFACE, IPMI_WRITE_FRU,
			 fru_write_complete, NULL, NULL, FRU_DATA_SIZE + 5, 2);
	if (!msg)
		return OPAL_RESOURCE;

	msg->data[0] = fru_dev_id;	/* FRU Device ID */
	msg->data[1] = 0x0;		/* Offset LSB (we always write a new common header) */
	msg->data[2] = 0x0;		/* Offset MSB */
	len = fru_add(&msg->data[3], FRU_DATA_SIZE);

	if (len < 0)
		return len;

	/* Three bytes for the actual FRU Data Command */
	msg->data[WRITE_INDEX] = 0;
	msg->data[REMAINING] = len;
	msg->req_size = min(len + 3, IPMI_MAX_REQ_SIZE);
	return ipmi_queue_msg(msg);
}

void ipmi_fru_init(u8 dev_id)
{
	fru_dev_id = dev_id;
	fru_write();

	return;
}
