#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;
}