/*
 * Routines for programming EPI3 personalities (specific to PSUs)
 */

#include <common.h>
#include <config.h>
#include <command.h>
#include <epi3.h>
#include <diag.h>
#include <malloc.h>
#include <bootmenu.h>
#include <pci.h>
#include <i2c.h>
#include <linux/ctype.h>
#include "diag_epi3_module.h"

static int
get_device_address (int psu, u8 *bus, u8 *device, int *alen)
{
#ifdef CONFIG_X610
	switch (psu) {
	case 1:
		*bus = 5;
		*device = 0x51;
		*alen = 2;
		return 0;
	default:
		return -1;
	}
#endif
	return -1;
}

static int
i2c_read_retry(u8 dev, uint addr, int alen, u8 *data, int length)
{
	int retries = 0;
	int max_retries = 100;

	while (max_retries > ++retries) {
		if (i2c_read (dev, addr, alen, data, length) == 0)
			return 0;

		diag_printf_always ("Retry i2c_read: dev=0x%02X addr=0x%02X dest=0x%08X\n",
				dev, addr, data);
		udelay (3000);
	}

	diag_printf_always ("Error: Exceeded maximum number of i2c retries (Read)\n");

	return -1;
}

static int
i2c_write_retry(u8 dev, uint addr, int alen, u8 *data, int length)
{
	int retries = 0;
	int max_retries = 100;

	while (max_retries > ++retries) {
		if (i2c_write (dev, addr, alen, data, length) == 0)
			return 0;

		// Seems to happen once for every write to ATMEL devices.
		// diag_printf_always ("Retry i2c_write: dev=0x%02X addr=0x%02X data=0x%08X\n",
		// 		dev, addr, data);
		udelay (3000);
	}

	diag_printf_always ("Error: Exceeded maximum number of i2c retries (Write)\n");

	return -1;
}

static int
diag_epi3_read_psu_eeprom(int psu, epi3Personality *personality)
{
	u8 bus, device;
	int alen;
	int offset = 0;
	int ret;

	// get the device address
	if (get_device_address (psu, &bus, &device, &alen) != 0) {
		diag_printf ("Error: could not determine i2c address for psu %d\n", psu);
		return -1;
	}

	// select the i2c bus
	ret = i2c_set_bus_num(bus);
	if (ret) {
		diag_printf ("Error: Failure changing bus number (%d)\n", ret);
		return -1;
	}

	// read the data 1 byte at a time
	do {
		ret = i2c_read_retry(device, offset, alen, (u8 *) ((u32) personality + offset), 0x01);
		if (ret != 0) {
			diag_printf_always ("Error: i2c read of device 0x%x on bus %d failed\n", device, bus);
			return -1;
		}
		offset++;

	} while (offset < sizeof (epi3Personality));

	return 0;
}

static int
diag_epi3_write_psu_personality(int psu, epi3Personality *personality)
{
	u8 bus, device;
	int alen;
	int offset = 0;
	int ret;

	// get the device address
	if (get_device_address (psu, &bus, &device, &alen) != 0) {
		diag_printf ("Error: could not determine i2c address for psu %d\n", psu);
		return -1;
	}

	// select the i2c bus
	ret = i2c_set_bus_num(bus);
	if (ret) {
		diag_printf ("Error: Failure changing bus number (%d)\n", ret);
		return -1;
	}

	// write the data 1 byte at a time
	do {
		ret = i2c_write_retry(device, offset, alen, (u8 *) ((u32) personality + offset), 0x01);
		if (ret != 0) {
			diag_printf_always ("Error: i2c read of device 0x%x on bus %d failed\n", device, bus);
			return -1;
		}
		offset++;

	} while (offset < sizeof (epi3Personality));

	return 0;
}

static int
diag_epi3_erase_psu_personality(int psu)
{
	u8 blank = 0xff;
	u8 bus, device;
	int alen;
	int offset = 0;
	int ret;

	// get the device address
	if (get_device_address (psu, &bus, &device, &alen) != 0) {
		diag_printf ("Error: could not determine i2c address for psu %d\n", psu);
		return -1;
	}

	// select the i2c bus
	ret = i2c_set_bus_num(bus);
	if (ret) {
		diag_printf ("Error: Failure changing bus number (%d)\n", ret);
		return -1;
	}

	// write the data 1 byte at a time
	do {
		ret = i2c_write_retry(device, offset, alen, &blank, 0x01);
		if (ret != 0) {
			diag_printf_always ("Error: i2c read of device 0x%x on bus %d failed\n", device, bus);
			return -1;
		}
		offset++;

	} while (offset < sizeof (epi3Personality));


	return 0;
}


static int diag_epi3_display_psu (int psu)
{
	epi3Personality *personality;

	personality = malloc (sizeof(epi3Personality));

	if (diag_epi3_read_psu_eeprom(psu, personality) != 0) {
		diag_printf ("Error: could not read personality\n");
		return 1;

	};

	if (epi3_dataok (personality))
	{
		diag_printf ("Error: No valid personality present\n");
		return 1;
	}

	diag_epi3_print_personality (personality);

	return 0;
}
	
static int diag_epi3_program_psu (int psu)
{
	epi3Personality *last_personality;
	int err = 0;
	char buf[256];
	char params[128];
	char key;

	last_personality = malloc (sizeof (epi3Personality));
	if (diag_epi3_read_psu_eeprom(psu, last_personality) != 0) {
		diag_printf ("Error: could not read personality\n");
		return 1;

	};

	if (epi3_dataok (last_personality)) {
		free (last_personality);
		last_personality = NULL;
	}

	if (diag_epi3_personality_menu (params, last_personality)) {
		err = -1;
		goto out;
	}

	// program personality by using the epi3psumk command
	sprintf (buf, "epi3psumk.%u %s", psu, params);

	diag_printf ("\n  Quick command to generate this personality "
			"in future (using CLI):\n\n  %s\n\n", buf);
	diag_printf ("  Write this personality to PSU now? (Y/N) ==> ");

	key = serial_getc ();
	if (toupper(key) != 'Y')
		goto out;

	diag_printf ("\n\n");

	run_command (buf, 0);

out:
	if (last_personality)
		free (last_personality);
	return err;

}

static int diag_epi3_erase_psu_personality_area (int doit, int psu)
{
	char key = 0;

	if (!doit)
	{
		diag_printf (
				"\n  WARNING: This option will destroy the contents of the PSU personality EEPROM."
				"\n  The personality should not be erased under any normal circumstance."
				"\n  Press Y to proceed, or any other key to return to the previous menu ==> ");

		key = serial_getc ();

		if (isalnum(key))
			serial_putc (key);
	}
	diag_printf ("\n");

	if (toupper(key) == 'Y' || doit) {
		if (diag_epi3_erase_psu_personality (psu)) {
			diag_printf ("Error: Failed to erase EEPROM\n");
			return 1;
		}

	}
	return 0;
}

static int
cmd_get_instance(char *str)
{
	char *val = strchr(str, '.');
	if (!val)
		return -1;

	val++;
	return simple_strtoul(val, NULL, 10);
}


static struct epi3_mod_ops psu_funcs = {
	.display = diag_epi3_display_psu,
	.program = diag_epi3_program_psu,
	.erase = diag_epi3_erase_psu_personality_area,
};

int
diag_epi3_psu_init(void)
{
	return diag_epi3_module_register (EPI3_TYPE_PSU, &psu_funcs);
}

/**
 * Command handler for epi3psumk command.Writes the EPI3 personality on a PSU.
 *
 * @param cmdtp command internal data
 * @param flag command flag
 * @param argc number of arguments supplied to the command
 * @param argv arguments list
 * @return 0 on success, 1 on input error, -1 on hardware error.
 */
int
do_epi3_epi3psumk (cmd_tbl_t * cmdtp, int flag, int argc, char *argv[])
{
	int psu;
	int err = 0;
	epi3Personality *new_personality;
	epi3Personality *last_personality;

	psu = cmd_get_instance(argv[0]);
	if (psu < 1 || psu > CONFIG_MAX_PSU) {
		diag_printf_always ("Error: No/Invalid instance specified\n");
		return 1;
	}

	if (argc < 2) {
		diag_printf_always ("Error: Too few parameters."
				"Type 'help epi3psumk' for more information.\n");
		return 1;
	}

	last_personality = calloc (sizeof (epi3Personality), 1);
	new_personality = calloc (sizeof (epi3Personality), 1);
	new_personality->epiVersion = 0x01;
	new_personality->epiMagicNumber = 0x55;

	// read the old personality
	if (diag_epi3_read_psu_eeprom (psu, last_personality)) {
		diag_printf_always ("Error: Failed to read existing personality\n");
		err = -1;
		goto out;
	}

	// old personality is not mandatory
	if (epi3_dataok (last_personality) != 0) {
		free (last_personality);
		last_personality = NULL;
	}

	if (diag_epi3mk_backend (argc, argv, last_personality, new_personality)) {
		err = 1;
		goto out;
	}

	// write the personality out
	if (diag_epi3_write_psu_personality (psu, new_personality))
	{
		diag_printf_always ("Error: Write failed\n");
		err = -1;
		goto out;
	}

	// re-read the newly programmed personality
	if (diag_epi3_read_psu_eeprom (psu, last_personality)) {
		diag_printf_always ("Error: Failed to read existing personality\n");
		err = -1;
		goto out;
	}

	// verify what we just read is what we expected to program above
	if (last_personality->epiChecksum != new_personality->epiChecksum) {
		diag_printf_always ("Error: Programmed personality checksum is incorrect\n");
		err = -1;
		goto out;
	}

	diag_printf_always ("Successfully wrote EPI3 information to PSU: %d\n",
			psu);

out:
	if (new_personality)
		free (new_personality);
	if (last_personality)
		free(last_personality);

	return err;
}

U_BOOT_CMD (epi3psumk, 11, 2, do_epi3_epi3psumk,
	    "epi3psumk\t- Make an EPI3 block in a PSU\n",
	    "[.N] [Serial] [Mfg] [Year] [Month] [BoardID] [PCBRev] [MAC] [844Bom] [OEM] [HWModLevel]\n\n"
	    "Standard command usage:\n\n"
	    "\tepi3psumk.1 123456789 A 06 06 270 X2 001122334455 A U 1\n\n"
	    "The 'Serial' parameter is required but ignored. The only case where this\n"
	    "parameter is observed is when the personality area is blank (factory first\n"
	    "program).\n\n"

	    "If a parameter is specified as a dash '-' character, then the value is\n"
	    "copied from the last valid personality.There is no need to insert the '-'\n"
	    "character in place of values after any of those you wish to modify.\n"
	    "For example, the command:\n\n"
	    "\tepi3psumk - - - - 270\n\n"
	    "Will make a new personality, with all attributes unchanged apart from the\n"
	    "specified BoardID.\n");
