#include <linux/init.h>
#include <linux/module.h>
#include <linux/pci.h>

#define VOODOO2 "voodoo2"

static struct pci_device_id voodoo2_id_table[] = {
    { PCI_DEVICE(0x121a, 0x0002) },
    { 0,}
};

MODULE_DEVICE_TABLE(pci, voodoo2_id_table);

static int voodoo2_probe(struct pci_dev *pdev, const struct pci_device_id *ent);
static void voodoo2_remove(struct pci_dev *pdev);

/* Module registration structure */
static struct pci_driver voodoo2 = {
    .name = VOODOO2,
    .id_table = voodoo2_id_table,
    .probe = voodoo2_probe,
    .remove = voodoo2_remove
};

/* This is a "private" data structure */
/* You can store there any data that should be passed between module's functions */
struct voodoo2_priv {
  volatile u32 __iomem *hwmem;
};

/* */

static int __init voodoo2_module_init(void)
{
    return pci_register_driver(&voodoo2);
}

static void __exit voodoo2_module_exit(void)
{
    pci_unregister_driver(&voodoo2);
}

void release_device(struct pci_dev *pdev);

void release_device(struct pci_dev *pdev)
{
    /* Free memory region */
    pci_release_region(pdev, pci_select_bars(pdev, IORESOURCE_MEM));
    /* And disable device */
    pci_disable_device(pdev);
}

struct name_int {
  const char * name;
  int offset;
};

#define STATUS 0x000
#define FBIINIT0 0x210
#define FBIINIT1 0x214
#define FBIINIT2 0x218
#define FBIINIT3 0x21c
#define FBIINIT4 0x200
#define FBIINIT5 0x244
#define FBIINIT6 0x248
#define FBIINIT7 0x24c
#define DACDATA 0x22c

#define LFBMODE 0x114

#define BACKPORCH 0x208
#define VIDEODIMENSIONS 0x20c
#define HSYNC 0x220
#define VSYNC 0x224

const struct name_int registers[] = {
  {"status", STATUS},
  {"fbiInit0", FBIINIT0},
  {"fbiInit1", FBIINIT1},
  {"fbiInit2", FBIINIT2},
  {"fbiInit3", FBIINIT3},
  {"fbiInit4", FBIINIT4},
  {"fbiInit5", FBIINIT5},
  {"fbiInit6", FBIINIT6},
  {"fbiInit7", FBIINIT7},
  //
  {"backPorch", BACKPORCH},
  {"videoDimensions", VIDEODIMENSIONS},
  {"hSync", HSYNC},
  {"vSync", VSYNC},
};

static inline void dac_data(volatile uint32_t * base, int data, int rs, int read)
{
  int value = 0
    | ((data & 0xff) << 0)
    | ((rs & 0b111) << 8)
    | ((read & 0b1) << 11);

  //printk(KERN_INFO "DACDATA = %08x\n", value);
  base[DACDATA / 4] = value;
}

#define DAC__RS__PIXEL_ADDRESS_WRITE  0b000 // TDV_GENDAC_WR
#define DAC__RS__PIXEL_ADDRESS_READ   0b011 // TDV_GENDAC_RD
#define DAC__RS__COLOR_VALUE          0b001 // TDV_GENDAC_LUT
#define DAC__RS__PIXEL_MASK           0b010 // TDV_GENDAC_PIXMASK
#define DAC__RS__PLL_ADDRESS_WRITE    0b100 // TDV_GENDAC_PLLWR
#define DAC__RS__PLL_PARAMETER        0b101 // TDV_GENDAC_PLLDATA
#define DAC__RS__COMMAND              0b110 // TDV_GENDAC_CMD
#define DAC__RS__PLL_ADDRESS_READ     0b111 // TDV_GENDAC_PLLRD

#define CLK0_f0_PLL 0x00
#define CLK0_f1_PLL 0x01
#define CLK0_f2_PLL 0x02
#define CLK0_f3_PLL 0x03
#define CLK0_f4_PLL 0x04
#define CLK0_f5_PLL 0x05
#define CLK0_f6_PLL 0x06
#define CLK0_f7_PLL 0x07
#define CLK1_fA_PLL 0x0A
#define CLK1_fB_PLL 0x0B
#define PLL_CONTROL 0x0E

const struct name_int dac_registers[] = {
  {"CLK0_f0_PLL", CLK0_f0_PLL},
  {"CLK0_f1_PLL", CLK0_f1_PLL},
  {"CLK0_f2_PLL", CLK0_f2_PLL},
  {"CLK0_f3_PLL", CLK0_f3_PLL},
  {"CLK0_f4_PLL", CLK0_f4_PLL},
  {"CLK0_f5_PLL", CLK0_f5_PLL},
  {"CLK0_f6_PLL", CLK0_f6_PLL},
  {"CLK0_f7_PLL", CLK0_f7_PLL},
  {"CLK1_fA_PLL", CLK1_fA_PLL},
  {"CLK1_fB_PLL", CLK1_fB_PLL},
  {"PLL_CONTROL", PLL_CONTROL},
};

#define MAXLOOP 4096

static void
busy_wait(volatile uint32_t * base)
{
  uint32_t x, cnt;
  cnt = 0;
  for (x = 0; x < MAXLOOP; x++) {
    if (base[STATUS / 4] & (1 << 7)) // busy
      cnt = 0;
    else
      cnt++;

    if (cnt >= 5)	/* Voodoo2 specs suggest at least 3 */
      break;
  }

  if (x == MAXLOOP)
    printk(KERN_INFO "wait timeout\n");
}

static inline void dac_write_8(volatile uint32_t * base, int param_address, int m)
{
  dac_data(base, param_address, DAC__RS__PLL_ADDRESS_WRITE, 0);
  busy_wait(base);

  dac_data(base, m, DAC__RS__PLL_PARAMETER, 0);
  busy_wait(base);
}

static inline void dac_write_16(volatile uint32_t * base, int param_address, int m, int n)
{
  dac_data(base, param_address, DAC__RS__PLL_ADDRESS_WRITE, 0);
  busy_wait(base);

  dac_data(base, m, DAC__RS__PLL_PARAMETER, 0);
  busy_wait(base);

  dac_data(base, n, DAC__RS__PLL_PARAMETER, 0);
  busy_wait(base);
}


struct m_n {
  uint8_t m;
  uint8_t n;
};

static inline struct m_n dac_read_16(volatile uint32_t * base, int param_address)
{
  dac_data(base, param_address, DAC__RS__PLL_ADDRESS_READ, 0);
  busy_wait(base);

  dac_data(base, 0, DAC__RS__PLL_PARAMETER, 1);
  busy_wait(base);
  uint32_t m = base[FBIINIT2 / 4] & 0xff;

  dac_data(base, 0, DAC__RS__PLL_PARAMETER, 1);
  busy_wait(base);
  uint32_t n = base[FBIINIT2 / 4] & 0xff;

  return (struct m_n){ m, n };
}

/* This function is called by the kernel */
static int voodoo2_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
    int bar, err;
    u16 vendor, device;
    unsigned long mmio_start, mmio_len;
    struct voodoo2_priv *drv_priv;

    /* Let's read data from the PCI device configuration registers */
    pci_read_config_word(pdev, PCI_VENDOR_ID, &vendor);
    pci_read_config_word(pdev, PCI_DEVICE_ID, &device);

    printk(KERN_INFO "vid: %04x pid: %04x\n", vendor, device);

    uint32_t init_enable;
    init_enable = 0
      | (1 << 2)  // enable writes to hardware initialization registers
      | (1 << 0); // remap fbiinit2, fbiinit3 to dacread, videochecksum
    pci_write_config_dword(pdev, 0x40, init_enable);
    pci_read_config_dword(pdev, 0x40, &init_enable);
    printk(KERN_INFO "init_enable: %08x\n", init_enable);

    /* Request IO BAR */
    bar = pci_select_bars(pdev, IORESOURCE_MEM);

    /* Enable device memory */
    err = pci_enable_device_mem(pdev);
    if (err) {
        printk(KERN_INFO "pci_enable_device_mem error\n");
        return err;
    }

    /* Request memory region for the BAR */
    err = pci_request_region(pdev, bar, VOODOO2);
    if (err) {
        printk(KERN_INFO "pci_request_region error\n");
        pci_disable_device(pdev);
        return err;
    }

    /* Get start and stop memory offsets */
    mmio_start = pci_resource_start(pdev, 0);
    mmio_len = pci_resource_len(pdev, 0);
    printk(KERN_INFO "mmio_start %p mmio_len %p\n", (void*)mmio_start, (void*)mmio_len);

    /* Allocate memory for the module private data */
    drv_priv = kzalloc(sizeof(struct voodoo2_priv), GFP_KERNEL);

    if (!drv_priv) {
        release_device(pdev);
        return -ENOMEM;
    }

    /* Remap BAR to the local pointer */
    drv_priv->hwmem = ioremap(mmio_start, mmio_len);
    if (!drv_priv->hwmem) {
       release_device(pdev);
       return -EIO;
    }

    /* Set module private data */
    /* Now we can access mapped "hwmem" from the any module's function */
    pci_set_drvdata(pdev, drv_priv);

    for (int i = 0; i < (sizeof (registers)) / (sizeof (registers[0])); i++) {
      printk(KERN_INFO "%s %08x\n",
             registers[i].name,
             drv_priv->hwmem[registers[i].offset / 4]);
    }

    //return 0;

    init_enable = 0
      | (1 << 0); // enable writes to hardware initialization registers
    pci_write_config_dword(pdev, 0x40, init_enable);
    busy_wait(drv_priv->hwmem);

    drv_priv->hwmem[FBIINIT1 / 4] = (1 << 8); // 8: Video Timing Reset (0=run, 1=reset).
    busy_wait(drv_priv->hwmem);
    drv_priv->hwmem[FBIINIT0 / 4] = (1 << 1)  // 1: Chuck Graphics Reset
                                  | (1 << 2); // 2: Chuck FIFO Reset
    busy_wait(drv_priv->hwmem);

    drv_priv->hwmem[FBIINIT2 / 4] &= (1 << 22); // 22: dram refresh disable
    busy_wait(drv_priv->hwmem);

    init_enable = 0
      | (1 << 2)  // remap fbiinit2, fbiinit3 to dacread, videochecksum
      | (1 << 0); // enable writes to hardware initialization registers
    pci_write_config_dword(pdev, 0x40, init_enable);
    busy_wait(drv_priv->hwmem);

    for (int i = 0; i < (sizeof (dac_registers)) / (sizeof (dac_registers[0])); i++) {
      struct m_n res = dac_read_16(drv_priv->hwmem, dac_registers[i].offset);
      printk(KERN_INFO "dac read1: %s: m: %02x n: %02x\n", dac_registers[i].name, res.m, res.n);
    }

    /* pixel clock: 40MHz
       graphics/memory clock: 44.6MHz
     */
    dac_data(drv_priv->hwmem, (0b0101 << 4), DAC__RS__COMMAND, 0); //write
    busy_wait(drv_priv->hwmem);
    dac_data(drv_priv->hwmem, 0, DAC__RS__COMMAND, 1); //read
    busy_wait(drv_priv->hwmem);
    uint32_t command = drv_priv->hwmem[FBIINIT2 / 4] & 0xff;
    printk(KERN_INFO "dac read2: RS__COMMAND: m: %02x\n", command);

    int m = 54;
    int n = 67;
    dac_write_16(drv_priv->hwmem, CLK0_f0_PLL, m, n);
    struct m_n res0 = dac_read_16(drv_priv->hwmem, CLK0_f0_PLL);
    printk(KERN_INFO "dac read2: CLK0_f0_PLL: m: %02x n: %02x\n", res0.m, res0.n);

    uint8_t pll_control = 0
      | (0 << 0)  // internal select: f0
      | (0 << 4)  // clk1 sel: fA
      | (1 << 5); // enable incs: 1
    dac_write_8(drv_priv->hwmem, PLL_CONTROL, pll_control);
    struct m_n res = dac_read_16(drv_priv->hwmem, PLL_CONTROL);
    printk(KERN_INFO "dac read2: PLL_CONTROL: m: %02x n: %02x\n", res.m, res.n);

    /*
      fbiInit0 00000410
        4:  Stall PCI enable for High Water Mark
        6:  (0x10) PCI FIFO Empty Entries Low Water Mark
      fbiInit1 00201102
        1:  One wait state
        8:  Video Timing Reset
        12: Software Blanking Enable
        20: (0x2) vid_clk_2x_sel
      fbiInit2 80000002
        1: DRAM banking configuration (0=128Kx16 banking, 1=256Kx16 banking)
        23: 0x100 Refresh_Load value
      fbiInit3 00000000
      fbiInit4 00000001
        0: Wait state cycles for PCI read accesses (0=1 ws, 1=2 ws).
      fbiInit5 00000128

      fbiInit6 001e6000
      fbiInit7 000000aa
    */

    int x_tiles = 832 / 32;

    // fbinit0
    int fbiinit0 = 0
      | (1 << 0) // no VGA passthrough
      | (0 << 1) // no chuck graphics reset
      | (0 << 2) // no chuck fifo reset
      | (0 << 3) // no byte swizzle
      | (1 << 4) // Stall PCI enable for High Water Mark
      | (0x10 << 6) // PCI FIFO Empty Entries Low Water Mark
      ;

    // fbiinit1
    int fbiinit1 = 0
      //| (1 << 1) // one wait state
      | (1 << 3) // enable linear framebuffer reads
      | (((x_tiles >> 1) & 0b1111) << 4) // tiles x [4:1]
      | (1 << 13) // drive data output
      | (1 << 14) // drive blank output
      | (1 << 15) // drive hsync/vsync output
      | (1 << 16) // drive dclk output
      | (0 << 17) // Video timing vclk input select : vid_clk_2x
      | (0x2 << 20) // Video timing vclk source select : vid_clk_2x
      | (((x_tiles >> 5) & 0b1) << 24) // tiles x [5]
      ;

    int fbiinit2 = 0
      | (1 << 1) // DRAM banking configuration (0=128Kx16 banking, 1=256Kx16 banking)
      | (1 << 5) // fast RAS
      | (1 << 6) // DRAM_OE
      | (247 << 11) // video buffer offset
      | (1 << 21) // FIFO RDA
      | (1 << 22) // Refresh Enable (0=disable, 1=enable). Default is 0.
      | (0x30 << 23) // Refresh_Load Value.
      ;

    int fbiinit3 = 0
      | (1 << 6) // trex dis
      ;

    int fbiinit4 = 0
      | (1 << 1) // lfb rda
      ;

    int fbiinit5 = 0
      | 0x00000128
      | (1 << 23)
      | (1 << 24)
      ;

    int fbiinit6 = 0
      | (x_tiles & 1) << 30; // tiles x [0]

    int yHeight = 600;
    int xWidth = 800;

    // sc->sc_videomode->vtotal - sc->sc_videomode->vsync_end
    int vBackPorch = 628 - 605;
    // sc->sc_videomode->htotal - sc->sc_videomode->hsync_end
    int hBackPorch = 1056 - 968;

    // sc->sc_videomode->vsync_end - sc->sc_videomode->vsync_start
    int vSyncOn = 605 - 601;
    // sc->sc_videomode->hsync_end - sc->sc_videomode->hsync_start
    int hSyncOn = 968 - 840;

    // sc->sc_videomode->vtotal - vsyncon
    int vSyncOff = 628 - vSyncOn;
    // sc->sc_videomode->htotal - hsyncon
    int hSyncOff = 1056 - hSyncOn;

    int hSync = 0
      | (((hSyncOn - 1) & 0x1ff) << 0)
      | ((hSyncOff & 0x3ff) << 16)
      ;

    int vSync = 0
      | ((vSyncOn & 0x1fff) << 0)
      | ((vSyncOff & 0x1fff) << 16)
      ;

    int backPorch = 0
      | (((hBackPorch - 2) & 0x1ff) << 0)
      | ((vBackPorch & 0x1ff) << 16)
      ;

    int videoDimensions = 0
      | (((xWidth - 1) & 0x7ff) << 0)
      | ((yHeight & 0x7ff) << 16)
      ;

    drv_priv->hwmem[HSYNC / 4] = hSync;
    busy_wait(drv_priv->hwmem);
    drv_priv->hwmem[VSYNC / 4] = vSync;
    busy_wait(drv_priv->hwmem);
    drv_priv->hwmem[BACKPORCH / 4] = backPorch;
    busy_wait(drv_priv->hwmem);
    drv_priv->hwmem[VIDEODIMENSIONS / 4] = videoDimensions;
    busy_wait(drv_priv->hwmem);

    int lfbMode = 0;

    drv_priv->hwmem[LFBMODE / 4] = lfbMode;
    busy_wait(drv_priv->hwmem);

    init_enable = 0
      | (1 << 0); // enable writes to hardware initialization registers
    pci_write_config_dword(pdev, 0x40, init_enable);
    busy_wait(drv_priv->hwmem);

    drv_priv->hwmem[FBIINIT0 / 4] = fbiinit0;
    drv_priv->hwmem[FBIINIT1 / 4] = fbiinit1;
    drv_priv->hwmem[FBIINIT2 / 4] = fbiinit2;
    drv_priv->hwmem[FBIINIT3 / 4] = fbiinit3;
    drv_priv->hwmem[FBIINIT4 / 4] = fbiinit4;
    drv_priv->hwmem[FBIINIT5 / 4] = fbiinit5;
    drv_priv->hwmem[FBIINIT6 / 4] = fbiinit6;
    busy_wait(drv_priv->hwmem);

    init_enable = 0
      | (1 << 1); // Enable writes to PCI FIFO
    pci_write_config_dword(pdev, 0x40, init_enable);
    busy_wait(drv_priv->hwmem);
    pci_write_config_dword(pdev, 0xC0, 0); // VCLK_ENABLE_REG

    for (int i = 0; i < (sizeof (registers)) / (sizeof (registers[0])); i++) {
      printk(KERN_INFO "read2: %s %08x\n",
             registers[i].name,
             drv_priv->hwmem[registers[i].offset / 4]);
    }
    printk(KERN_INFO "hsync: %08x vsync: %08x\n", hSync, vSync);

#define rgb(r, g, b) (((r) << 11) | ((g) << 5) | ((b) << 0))
    static const uint16_t panda[] __attribute__((aligned(4))) = {
      //#embed "panda.rgb565.txt"
      #include "panda.txt"
    };
    
    const int colors[] = {
      rgb(31, 0, 0),
      rgb(0, 63, 0),
      rgb(0, 0, 31),
      rgb(31, 63, 0), // yellow
      rgb(31, 0, 31), // magenta
      rgb(0, 63, 31), // cyan
      rgb(31, 31, 0), // orange
      rgb(31, 0, 15), // pink
      rgb(0, 31, 31), // light blue
      rgb(0, 63, 15), // light green
    };

    if (0) {
      for (int y = 0; y < 600; y++) {
	for (int x = 0; x < 1024; x++) {
	  ((volatile uint16_t *)drv_priv->hwmem)[(0x0400000 / 2) + y * 1024 + x] = 0
	    | colors[(y / 32) % 10];

	  if (0) {
	    ((volatile uint16_t *)drv_priv->hwmem)[(0x0400000 / 2) + y * 1024 + x] = 0
	      | rgb(0, 63, 0);
	  }
	}
	busy_wait(drv_priv->hwmem);
      }
      for (int y = 0; y < 600; y++) {
	((volatile uint16_t *)drv_priv->hwmem)[(0x0400000 / 2) + y * 1024 + 10] = 0
	  | rgb(0, 0, 31);
	((volatile uint16_t *)drv_priv->hwmem)[(0x0400000 / 2) + y * 1024 + 790] = 0
	  | rgb(0, 63, 0);      
      }
    }

    for (int y = 0; y < 600; y++) {
      for (int x = 0; x < 800; x++) {
	((volatile uint16_t *)drv_priv->hwmem)[(0x0400000 / 2) + y * 1024 + x] = ((uint16_t*)panda)[y * 800 + x];
      }
    }

    return 0;
}

/* Clean up */
static void voodoo2_remove(struct pci_dev *pdev)
{
    struct voodoo2_priv *drv_priv = pci_get_drvdata(pdev);

    if (drv_priv) {
        if (drv_priv->hwmem) {
            iounmap(drv_priv->hwmem);
        }

        kfree(drv_priv);
    }

    release_device(pdev);
}

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Zachary Buhman <zack@buhman.org>");
MODULE_DESCRIPTION("Voodoo2 module");
MODULE_VERSION("0.1");

module_init(voodoo2_module_init);
module_exit(voodoo2_module_exit);
