#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <sys/io.h>
#include <errno.h>
#include <readline/readline.h>
#include <readline/history.h>
#include <linux/types.h>

typedef __u8 u8;
typedef __u16 u16;
typedef __u32 u32;

#include "hermes.h"

#define BAP_BUSY_TIMEOUT (50)
#define CMD_BUSY_TIMEOUT (100) /* In iterations of ~1us */
#define CMD_COMPL_TIMEOUT (20000) /* in iterations of ~10us */

#define fatal(s) do { perror(s); exit(1); } while (0)

typedef struct record_info {
	u16 rid;
	u16 len;
	char *name;
} record_info_t;

record_info_t rid_table[] = {
	{0xfc00,  2, "CNFPORTTYPE"},
	{0xfc01,  6, "CNFOWNMACADDR"},
	{0xfc02, 34, "CNFDESIREDSSID"},
	{0xfc03,  2, "CNFOWNCHANNEL"},
	{0xfc04, 34, "CNFOWNSSID"},
	{0xfc05,  2, "CNFOWNATIMWIN"},
	{0xfc06,  0, "CNFSYSSCALE"},
	{0xfc07,  0, "CNFMAXDATALEN"},
	{0xfc08,  0, "CNFWDSADDR"},
	{0xfc09,  0, "CNFPMENABLED"},
	{0xfc0a,  0, "CNFPMEPS"},
	{0xfc0b,  0, "CNFMULTICASTRX"},
	{0xfc0c,  0, "CNFMAXSLEEPDUR"},
	{0xfc0d,  0, "CNFCNFPMHOLDDUR"},
	{0xfc0e,  0, "CNFOWNNAME"},
	{0xfc10,  0, "CNFOWNDTIMPER"},
	{0xfc11,  0, "CNFWDSADDR1"},
	{0xfc12,  0, "CNFWDSADDR2"},
	{0xfc13,  0, "CNFWDSADDR3"},
	{0xfc14,  0, "CNFWDSADDR4"},
	{0xfc15,  0, "CNFWDSADDR5"},
	{0xfc16,  0, "CNFWDSADDR6"},
	{0xfc17,  0, "CNFMCASTPMBUFF"},
	{0xfcb0,  0, "CNFSHORTPREAMBLE"}
};

#define NUM_RIDS (sizeof(rid_table) / sizeof(record_info_t))

typedef struct record {
	u16 len;
	u16 type;
	u16 value[4096];
} record_t;


/* Set up a BAP to read a particular chunk of data from card's internal buffer.
 *
 * Returns: < 0 on internal failure (errno), 0 on success, >0 on error from firmware 
 *
 * Callable from any context */
static int hermes_bap_seek(ulong hw, int bap, u16 id, u16 offset)
{
	int sreg = bap ? HERMES_SELECT1 : HERMES_SELECT0;
	int oreg = bap ? HERMES_OFFSET1 : HERMES_OFFSET0;
	int k;
	u16 reg;

	/* Paranoia.. */
	if ( (offset > HERMES_BAP_OFFSET_MAX) || (offset % 2) )
		return -EINVAL;

	/* Now we actually set up the transfer */
	hermes_write_reg(hw, sreg, id);
	hermes_write_reg(hw, oreg, offset);
	/* Wait for the BAP to be ready */
	k = BAP_BUSY_TIMEOUT;
	reg = hermes_read_reg(hw, oreg);
	while ( (reg & HERMES_OFFSET_BUSY) && k) {
		k--;
		reg = hermes_read_reg(hw, oreg);
	}

	if (reg & HERMES_OFFSET_BUSY)
		return -ETIMEDOUT;
	if (reg & HERMES_OFFSET_ERR)
		return reg;

	return 0;
}

/* Read a block of data from the chip's buffer, via the
 * BAP. Synchronization/serialization is the caller's problem.  len
 * must be even.
 *
 * Returns: < 0 on internal failure (errno), 0 on success, > 0 on error from firmware
 *
 * Callable from any context */
int hermes_bap_pread(ulong hw, int bap, char *buf, u16 len,
		      u16 id, u16 offset)
{
	int dreg = bap ? HERMES_DATA1 : HERMES_DATA0;
	int err;

	err = hermes_bap_seek(hw, bap, id, offset);
	if (err)
		return err;

	/* Actually do the transfer */
	hermes_read_data(hw, dreg, buf, len/2);

	return 0;
}

/* Write a block of data to the chip's buffer, via the
 * BAP. Synchronization/serialization is the caller's problem. len
 * must be even.
 *
 * Returns: < 0 on internal failure (errno), 0 on success, > 0 on error from firmware
 *
 * Callable from any context */
int hermes_bap_pwrite(ulong hw, int bap, const char *buf, u16 len,
		       u16 id, u16 offset)
{
	int dreg = bap ? HERMES_DATA1 : HERMES_DATA0;
	int err;

	if (len % 2)
		return -EINVAL;

	err = hermes_bap_seek(hw, bap, id, offset);
	if (err)
		return err;

	/* Actually do the transfer */
	hermes_write_data(hw, dreg, buf, len/2);

	return 0;
}

void usage(void)
{
	fprintf(stderr, "Usage: userprism <base io> [showrecords]\n");
	exit(1);
}

char *
rl_gets (void)
{
	char * line;

	/* Get a line from the user. */
	line = readline ("> ");
	
	/* If the line has any text in it, save it on the history. */
	if (line && *line)
		add_history (line);
	
	return (line);
}

void display(unsigned long base)
{
#define DREG(name) printf("%-16s: %04x\n", #name, inw(base + HERMES_##name))
	DREG(CMD);
	DREG(PARAM0);
	DREG(PARAM1);
	DREG(PARAM2);
	DREG(STATUS);
	DREG(RESP0);
	DREG(RESP1);
	DREG(RESP2);
	DREG(INFOFID);
	DREG(RXFID);
	DREG(ALLOCFID);
	DREG(TXCOMPLFID);
	DREG(SELECT0);
	DREG(OFFSET0);
	DREG(SELECT1);
	DREG(OFFSET1);

	DREG(EVSTAT);
	DREG(INTEN);
	DREG(EVACK);
	DREG(CONTROL);
	DREG(SWSUPPORT0);
	DREG(SWSUPPORT1);
	DREG(SWSUPPORT2);
	DREG(AUXPAGE);
	DREG(AUXOFFSET);
	DREG(AUXDATA);
#undef DREG
}

#undef udelay
void udelay(ulong usec) {
	struct timespec ts;

	ts.tv_sec = 0;
	ts.tv_nsec = 1000 * usec;
	nanosleep(&ts, NULL);
}

static int hermes_issue_cmd(ulong base, u16 cmd, u16 param0)
{
	int k = CMD_BUSY_TIMEOUT;
	u16 reg;

	/* First wait for the command register to unbusy */
	reg = hermes_read_regn(base, CMD);
	while ( (reg & HERMES_CMD_BUSY) && k ) {
		k--;
		udelay(1);
		reg = hermes_read_regn(base, CMD);
	}
	if (reg & HERMES_CMD_BUSY) {
		return -EBUSY;
	}

	hermes_write_regn(base, PARAM2, 0);
	hermes_write_regn(base, PARAM1, 0);
	hermes_write_regn(base, PARAM0, param0);
	hermes_write_regn(base, CMD, cmd);
	
	return 0;
}

int hermes_docmd_wait(ulong base, u16 cmd, u16 parm0)
{
	int err;
	int k;
	u16 reg;
	u16 status;

	err = hermes_issue_cmd(base, cmd, parm0);
	if (err) {
		return -EIO;
	}

	reg = hermes_read_regn(base, EVSTAT);
	k = CMD_COMPL_TIMEOUT;
	while ( (! (reg & HERMES_EV_CMD)) && k) {
		k--;
		udelay(10);
		reg = hermes_read_regn(base, EVSTAT);
	}

	if (! (reg & HERMES_EV_CMD)) {
		return -ETIMEDOUT;
	}

	status = hermes_read_regn(base, STATUS);

	hermes_write_regn(base, EVACK, HERMES_EV_CMD);

	if (status & HERMES_STATUS_RESULT)
		return -EIO;

	return 0;
}

int read_lt(ulong base, int bap, u16 rid, record_t *rec)
{
	int err;

	err = hermes_docmd_wait(base, HERMES_CMD_ACCESS, rid);
	if (err) {
		fprintf(stderr, "Cannot access RID 0x%04x\n", rid);
		return err;
	}

	err = hermes_bap_pread(base, bap, (char *)rec, 4, rid, 0);
	if (err) {
		fprintf(stderr, "Error %d reading type/length\n", err);
		return err;
	}

	if (rid != rec->type) {
		fprintf(stderr, "RID (0x%04x) doesn't match rec->type (0x%04x)\n",
			rid, rec->type);
		return -1;
	}

	if (rec->len <= 1) {
		fprintf(stderr, "Record too short, rec->len = %d\n", rec->len);
		return err;
	}

	/* Convert length to bytes */
	rec->len = 2 * (rec->len - 1);

	err = hermes_bap_pread(base, bap, (char *)&rec->value, rec->len, rid, 4);
	if (err) {
		fprintf(stderr, "Error %d reading value (%d bytes)\n", err, rec->len);
		return err;
	}

	return 0;
}

void
show_records(unsigned long base)
{
	int i, j;
	record_t rec;

	printf("%d records total.\n", NUM_RIDS);
	for (i = 0; i < NUM_RIDS; i++) {
		record_info_t *info = &rid_table[i];
		printf("Record %s [0x%04x]:", info->name, info->rid);
		if (info->len)
			printf(" expected length %04d\n", info->len);
		else
			printf("\n");

		if (read_lt(base, 1, info->rid, &rec) != 0)
			continue;
		printf("Type 0x%04x, Len %04d: ", rec.type, rec.len);
		for (j = 0; j < rec.len / 2; j++)
			printf("%04x:", rec.value[j]);
		printf("\n");
	}
}

void
show_record(unsigned long base, u16 rid)
{
	record_t rec;
	int j;

	if (read_lt(base, 1, rid, &rec) != 0)
		return;
	printf("RID 0x%04x: ", rid);
	printf("type=0x%04x len=%04d value=", rec.type, rec.len);
	for (j = 0; j < rec.len / 2; j++)
		printf("%04x:", rec.value[j]);
	printf("\n");
}

void do_line(unsigned long base, const char *line)
{
	char reg[100];
	int off;
	int val;
	int n;

	n = sscanf(line, "%s %i\n", reg, &val);
	if (n < 1 || n > 2) {
		fprintf(stderr, "?\n");
		return;
	}

	if (strcasecmp(reg, "CMD") == 0) {
		off = HERMES_CMD;
	} else if (strcasecmp(reg, "PARAM0") == 0) {
		off = HERMES_PARAM0;
	} else if (strcasecmp(reg, "PARAM1") == 0) {
		off = HERMES_PARAM1;
	} else if (strcasecmp(reg, "PARAM2") == 0) {
		off = HERMES_PARAM2;
	} else if (strcasecmp(reg, "STATUS") == 0) {
		off = HERMES_STATUS;
	} else if (strcasecmp(reg, "RESP0") == 0) {
		off = HERMES_RESP0;
	} else if (strcasecmp(reg, "RESP1") == 0) {
		off = HERMES_RESP1;
	} else if (strcasecmp(reg, "RESP2") == 0) {
		off = HERMES_RESP2;
	} else if (strcasecmp(reg, "INFOFID") == 0) {
		off = HERMES_INFOFID;
	} else if (strcasecmp(reg, "RXFID") == 0) {
		off = HERMES_RXFID;
	} else if (strcasecmp(reg, "ALLOCFID") == 0) {
		off = HERMES_ALLOCFID;
	} else if (strcasecmp(reg, "TXCOMPLFID") == 0) {
		off = HERMES_TXCOMPLFID;
	} else if (strcasecmp(reg, "SELECT0") == 0) {
		off = HERMES_SELECT0;
	} else if (strcasecmp(reg, "OFFSET0") == 0) {
		off = HERMES_OFFSET0;
	} else if (strcasecmp(reg, "DATA0") == 0) {
		off = HERMES_DATA0;
	} else if (strcasecmp(reg, "SELECT1") == 0) {
		off = HERMES_SELECT1;
	} else if (strcasecmp(reg, "OFFSET1") == 0) {
		off = HERMES_OFFSET1;
	} else if (strcasecmp(reg, "DATA1") == 0) {
		off = HERMES_DATA1;
	} else if (strcasecmp(reg, "EVSTAT") == 0) {
		off = HERMES_EVSTAT;
	} else if (strcasecmp(reg, "INTEN") == 0) {
		off = HERMES_INTEN;
	} else if (strcasecmp(reg, "EVACK") == 0) {
		off = HERMES_EVACK;
	} else if (strcasecmp(reg, "CONTROL") == 0) {
		off = HERMES_CONTROL;
	} else if (strcasecmp(reg, "SWSUPPORT0") == 0) {
		off = HERMES_SWSUPPORT0;
	} else if (strcasecmp(reg, "SWSUPPORT1") == 0) {
		off = HERMES_SWSUPPORT1;
	} else if (strcasecmp(reg, "SWSUPPORT2") == 0) {
		off = HERMES_SWSUPPORT2;
	} else if (strcasecmp(reg, "AUXPAGE") == 0) {
		off = HERMES_AUXPAGE;
	} else if (strcasecmp(reg, "AUXOFFSET") == 0) {
		off = HERMES_AUXOFFSET;
	} else if (strcasecmp(reg, "AUXDATA") == 0) {
		off = HERMES_AUXDATA;
	} else if (strcasecmp(reg, ".") == 0) {
		display(base);
		return;
	} else if (strcasecmp(reg, ",") == 0) {
		if (n != 2) {
			fprintf(stderr, "? No FID given\n");
			return;
		}
		show_record(base, val);
		return;
	} else {
		fprintf(stderr, "? Unknown register \"%s\"\n", reg);
		return;
	}

	if (n == 2) {
		printf("0x%04x -> 0x%04x\n", val, (int)base+off);
		outw(val, base+off);
	}
	printf("%s (0x%04x) = 0x%04x\n", reg, off, inw(base+off));
}

int
main(int argc, char *argv[])
{
	char *e;
	unsigned long base;
	int err;
	char * line;
	
	if (argc < 2)
		usage();

	base = strtol(argv[1], &e, 0);
	if (*e)
		usage();

	err = ioperm(base, 0x40, 1);
	if (err != 0)
		fatal("ioperm");

	if (argc > 2) {
		show_records(base);
		exit(0);
	}
		

	while (1) {
		line = rl_gets();
		
		if (! line)
			break;

		do_line(base, line);

		free(line);
	};
	

	exit(0);
}
