#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <limits.h>
#include <errno.h>
#include <string.h>
#include <sys/mman.h>

#define DAC_REG 0x0bc06000
#define FBC_REG 0x04000000
#define EXP_ADDR 0x0bc18000

static uint32_t dac_addr[] = {
    0x0000,
    0x001,
    0x1000,
    0x002,
    0x2000,
    0x100,
    0x3100,
    0x020,
    0x3120,
    0x020,
    0x3150,
    0x003,
    0x5000,
    0x001,
    0x5001,
    0x001,
    0x6000,
    0x011,
    0x8000,
    0x003,
};
#define dac_addr_len (sizeof(dac_addr) / sizeof (dac_addr[0]))

static uint32_t fbc_addr[] = {
    0x00c,
    0x007,
    0x020,
    0x002,
    0x030,
    0x002,
    0x040,
    0x002,
    0x052,
    0x008,
    0x060,
    0x006,
    0x100,
    0x001,
    0x200,
    0x010,
    0x244,
    0x02a,
    0x380,
    0x001,
    0x800,
    0x001,
    0x900,
    0x001,
    0x980,
    0x001,
    0x10270,
    0x004
};
#define fbc_addr_len (sizeof(fbc_addr) / sizeof (fbc_addr[0]))

void
set_dac_reg(volatile uint32_t *dac_reg, volatile uint32_t *dac_reg_data,
    uint32_t reg, uint32_t val, int do_nothing)
{
	printf("    Setting DAC register 0x%04x value 0x%04x\n", reg, val);
	if (!do_nothing) {
		*dac_reg = reg;
		*dac_reg_data = val;
	}
}

void
set_fbc_reg(volatile uint32_t *fbc_reg, uint32_t reg, uint32_t val,
    int do_nothing)
{
	printf("    Setting FBC register 0x%04x value 0x%04x\n", reg, val);
	if (!do_nothing) {
		fbc_reg[reg / 4] = val;
	}
}

double pclk (uint32_t reg)
{
	int pd;

	pd = (reg >> 11) & 0x07;
	switch (pd) {
		case 0x03:
			pd = 8;
			break;
		case 0x02:
			pd = 4;
			break;
		case 0x01:
			pd = 2;
			break;
		case 0:
		default:
			pd = 1;
			break;
	}
	return 13500000 * (reg & 0x7f) / ((reg >> 7) & 0x0f) / pd;
}

#define FBNAME "/dev/fb0"

int
main (int argc, char *argv[])
{
	int ch, fb, i, j;
	char fbname[PATH_MAX] = FBNAME;
	int do_nothing = 0;
	volatile uint32_t *fbc_reg, *dac_reg, *dac_reg_data, res;
	volatile uint8_t *exp;
	uint8_t fem, ver;
	char buf[256], *bufp;
	uint32_t pfc, hbs, hbe, hss, tgc, vbs, vbe, vss, pll;
	int hres, vres, htot, vtot;
	double pc, vref, href;
	uint32_t reg, val;

	while ((ch = getopt(argc, argv, "d:n")) != -1) {
		switch (ch) {
			case 'd':
				strlcpy(fbname, optarg, PATH_MAX - 1);
				break;
			case 'n':
				do_nothing = 1;
				break;
			case '?':
				return 1;
				break;
		}
	}
	argc -= optind;
	argv += optind;

	fb = open(fbname, O_RDWR, 0);
	if (fb == -1) {
		fprintf(stderr, "Error opening FB: %s\n", strerror(errno));
		return 1;
	}

	/* Board type */
	fbc_reg = mmap(NULL, (size_t) 0x10280, PROT_READ | PROT_WRITE,
	    MAP_PRIVATE, fb, (off_t) FBC_REG);
	if (fbc_reg == MAP_FAILED) {
		fprintf(stderr, "Error mapping FBC: %s\n", strerror(errno));
		return 1;
	}
	exp = mmap(NULL, (size_t) 0x1, PROT_READ | PROT_WRITE,
	    MAP_PRIVATE, fb, (off_t) EXP_ADDR);
	if (exp == MAP_FAILED) {
		fprintf(stderr, "Error mapping EXP: %s\n", strerror(errno));
		return 1;
	}
	/* FloatEnableMask (byte offset 0x1540) */
	fem = (uint8_t) fbc_reg[0x550] & 0x7f;
	printf("Board type ");
	if (fem == 0x01 || fem == 0x07 || fem == 0x3f) {
		printf("%02x: ", fem);
		if (fem == 0x01)
			printf("Elite3D M3 or M6 (without firmware loaded)\n");
		else if (fem == 0x07)
			printf("Elite3D M3\n");
		else if (fem == 0x3f)
			printf("Elite3D M6\n");
		printf("    Z-buffer, Double-Buffered\n");
	} else {
		/*
		 * Strapping bits
		 * Need to read these twice, as occasionally we read
		 * a different value first time.
		 */
		ver = exp[0];
		ver = exp[0];
		/* Frame Buffer Config 0 (byte offset 0x270) */
		res = fbc_reg[0x9c];
		printf("0x%02x: ", ver);
		printf("Creator/Creator3D ");
		switch (ver & 0x78) {
			case 0x00:
				printf("(FFB1 prototype)\n");
				break;
			case 0x08:
				printf("(FFB1)\n");
				break;
			case 0x18:
				printf("(FFB1 Speedsort)\n");
				break;
			case 0x20:
				printf("(FFB2/vertical prototype)\n");
				break;
			case 0x28:
				printf("(FFB2/vertical)\n");
				break;
			case 0x30:
				printf("(FFB2+/vertical)\n");
				break;
			case 0x40:
				printf("(FFB2/horizontal)\n");
				break;
			case 0x50:
				printf("(FFB2+/horizontal)\n");
				break;
			default:
				printf("(unknown board version)\n");
				break;
		}
		bufp = &buf[0];
		strcpy(bufp, "  ");
		bufp += strlen("  ");
		if (ver & 0x04) {
			strcpy(bufp, "Stereo resolution");
			bufp += strlen("Stereo resolution");
		}
		if (ver & 0x02) {
			if (bufp != &buf[2]) {
				strcpy(bufp, ", ");
				bufp += strlen(", ");
			}
			strcpy(bufp, "Z-buffer");
			bufp += strlen("Z-buffer");
		}
		if (bufp != &buf[2]) {
			strcpy(bufp, ", ");
			bufp += strlen(", ");
		}
		if (ver & 0x01) {
			if ((res & 0x30) != 0x30) {
				strcpy(bufp, "Double-buffered");
				bufp += strlen("Double-buffered");
			} else {
				strcpy(bufp, "Single-buffered");
				bufp += strlen("Single-buffered");
			}
		} else {
			strcpy(bufp, "Single-buffered");
			bufp += strlen("Single-buffered");
		}
		*bufp = '\0';
		printf("%s\n", buf);
	}
	
	dac_reg = mmap(NULL, (size_t) 0x4, PROT_READ | PROT_WRITE,
	    MAP_PRIVATE, fb, (off_t) DAC_REG);
	if (dac_reg == MAP_FAILED) {
		fprintf(stderr, "Error mapping DAC: %s\n", strerror(errno));
		return 1;
	}
	dac_reg_data = dac_reg;
	dac_reg_data++;

	*dac_reg = 0x1000;
	pfc = *dac_reg_data;
	*dac_reg = 0x6007;
	hbs = *dac_reg_data;
	*dac_reg = 0x6006;
	hbe = *dac_reg_data;
	*dac_reg = 0x6009;
	hss = *dac_reg_data;
	*dac_reg = 0x6000;
	tgc = *dac_reg_data;
	*dac_reg = 0x6002;
	vbs = *dac_reg_data;
	*dac_reg = 0x6001;
	vbe = *dac_reg_data;
	*dac_reg = 0x6004;
	vss = *dac_reg_data;
	*dac_reg = 0x0000;
	pll = *dac_reg_data;

	if (pfc & 0x01)
		i = 4;	/* interleave = 8/2:1 */
	else
		i = 2; /* interleave = 4/2:1 */

	/* XXX: These are wrong in interlaced mode */
	/* XXX: How to check for stereo? */
	hres = (hbs * i) - (hbe * i);
	htot = (hss + 1) * i;
	if (tgc & 0x40)
		hres *= 2;	/* Interlaced */

	vres = vbs - vbe;
	vtot = vss + 1;

	pc = pclk(pll);
	href = pc / htot / 1000;	/* kHz */
	vref = pc / (vtot * htot);
	printf("Current resolution: %dx%dx%1.1f%s", hres, vres, vref,
	    tgc & 0x40 ? " interlaced" : "");
	printf(" (%.1lfkHz, %.2lfMHz)\n", href, pc / 1000000);

	printf("\nDAC register dump:\n");
	printf("Offset  value\n");
	for (i = 0; i < dac_addr_len; i +=2) {
		for (j = 0; j < (dac_addr[i + 1]); j++) {
			*dac_reg = dac_addr[i] + j;
			printf("  %04x  %08x\n", dac_addr[i] + j,
			    *dac_reg_data);
		}
	}

	printf("\nFBC register dump:\n");
	printf("Offset  value\n");
	for (i = 0; i < fbc_addr_len; i +=2) {
		for (j = 0; j < (fbc_addr[i + 1]); j++) {
			printf("  %04x %08x\n", fbc_addr[i] + j * 4,
			    fbc_reg[fbc_addr[i] / 4 + j]);
		}
	}

	if (argc != 2) {
		return 0;
	}
	reg = strtol(argv[0], NULL, 16);
	val = strtol(argv[1], NULL, 16);
	if (reg == 0 || reg >= 0x1000)
		set_dac_reg(dac_reg, dac_reg_data, reg, val, do_nothing);
	else
		set_fbc_reg(fbc_reg, reg, val, do_nothing);

	return 0;
}