#include "rpi_smi.h"

#include <unistd.h>

#include "../dma/rpi_dma.h"
#include "../gpio/rpi_gpio.h"

// GPIO first pin
#define SMI_SD0_PIN 8

// Data widths
#define SMI_8_BITS 0
#define SMI_16_BITS 1
#define SMI_18_BITS 2
#define SMI_9_BITS 3

// Clock registers and values
#define CLK_BASE (PHYS_REG_BASE + 0x101000)
// #define CLK_PWM_CTL 0xa0
// #define CLK_PWM_DIV 0xa4
#define CLK_SMI_CTL 0xb0
#define CLK_SMI_DIV 0xb4
#define CLK_PASSWD 0x5a000000
#define PWM_CLOCK_ID 0xa

// DMA request threshold
#define REQUEST_THRESH 2

// Register definitions
#define SMI_BASE (PHYS_REG_BASE + 0x600000)
#define SMI_CS 0x00   // Control & status
#define SMI_L 0x04    // Transfer length
#define SMI_A 0x08    // Address
#define SMI_D 0x0c    // Data
#define SMI_DSR0 0x10 // Read settings device 0
#define SMI_DSW0 0x14 // Write settings device 0
#define SMI_DSR1 0x18 // Read settings device 1
#define SMI_DSW1 0x1c // Write settings device 1
#define SMI_DSR2 0x20 // Read settings device 2
#define SMI_DSW2 0x24 // Write settings device 2
#define SMI_DSR3 0x28 // Read settings device 3
#define SMI_DSW3 0x2c // Write settings device 3
#define SMI_DMC 0x30  // DMA control
#define SMI_DCS 0x34  // Direct control/status
#define SMI_DCA 0x38  // Direct address
#define SMI_DCD 0x3c  // Direct data
#define SMI_FD 0x40   // FIFO debug
#define SMI_REGLEN (SMI_FD * 4)

// Union of 32-bit value with register bitfields
#define REG_DEF(name, fields)                                                                      \
  typedef union {                                                                                  \
    struct {                                                                                       \
      volatile uint32_t fields;                                                                    \
    };                                                                                             \
    volatile uint32_t value;                                                                       \
  } name

// Control and status register
#define SMI_CS_FIELDS                                                                              \
  enable:                                                                                          \
  1, done : 1, active : 1, start : 1, clear : 1, write : 1, _x1 : 2, teen : 1, intd : 1, intt : 1, \
      intr : 1, pvmode : 1, seterr : 1, pxldat : 1, edreq : 1, _x2 : 8, _x3 : 1, aferr : 1,        \
      txw : 1, rxr : 1, txd : 1, rxd : 1, txe : 1, rxf : 1
REG_DEF(SMI_CS_REG, SMI_CS_FIELDS);

// Data length register
#define SMI_L_FIELDS                                                                               \
  len:                                                                                             \
  32
REG_DEF(SMI_L_REG, SMI_L_FIELDS);

// Address & device number
#define SMI_A_FIELDS                                                                               \
  addr:                                                                                            \
  6, _x1 : 2, dev : 2
REG_DEF(SMI_A_REG, SMI_A_FIELDS);

// Data FIFO
#define SMI_D_FIELDS                                                                               \
  data:                                                                                            \
  32
REG_DEF(SMI_D_REG, SMI_D_FIELDS);

// DMA control register
#define SMI_DMC_FIELDS                                                                             \
  reqw:                                                                                            \
  6, reqr : 6, panicw : 6, panicr : 6, dmap : 1, _x1 : 3, dmaen : 1
REG_DEF(SMI_DMC_REG, SMI_DMC_FIELDS);

// Device settings: read (1 of 4)
#define SMI_DSR_FIELDS                                                                             \
  rstrobe:                                                                                         \
  7, rdreq : 1, rpace : 7, rpaceall : 1, rhold : 6, fsetup : 1, mode68 : 1, rsetup : 6, rwidth : 2
REG_DEF(SMI_DSR_REG, SMI_DSR_FIELDS);

// Device settings: write (1 of 4)
#define SMI_DSW_FIELDS                                                                             \
  wstrobe:                                                                                         \
  7, wdreq : 1, wpace : 7, wpaceall : 1, whold : 6, wswap : 1, wformat : 1, wsetup : 6, wwidth : 2
REG_DEF(SMI_DSW_REG, SMI_DSW_FIELDS);

// Direct control register
#define SMI_DCS_FIELDS                                                                             \
  enable:                                                                                          \
  1, start : 1, done : 1, write : 1
REG_DEF(SMI_DCS_REG, SMI_DCS_FIELDS);

// Direct control address & device number
#define SMI_DCA_FIELDS                                                                             \
  addr:                                                                                            \
  6, _x1 : 2, dev : 2
REG_DEF(SMI_DCA_REG, SMI_DCA_FIELDS);

// Direct control data
#define SMI_DCD_FIELDS                                                                             \
  data:                                                                                            \
  32
REG_DEF(SMI_DCD_REG, SMI_DCD_FIELDS);

// Debug register
#define SMI_FLVL_FIELDS                                                                            \
  fcnt:                                                                                            \
  6, _x1 : 2, flvl : 6
REG_DEF(SMI_FLVL_REG, SMI_FLVL_FIELDS);

// Pointers to SMI registers
volatile SMI_CS_REG *smi_cs;
volatile SMI_L_REG *smi_l;
volatile SMI_A_REG *smi_a;
volatile SMI_D_REG *smi_d;
volatile SMI_DMC_REG *smi_dmc;
volatile SMI_DSR_REG *smi_dsr;
volatile SMI_DSW_REG *smi_dsw;
volatile SMI_DCS_REG *smi_dcs;
volatile SMI_DCA_REG *smi_dca;
volatile SMI_DCD_REG *smi_dcd;

MEM_MAP smi_regs, clk_regs;

void setup_smi_dma(MEM_MAP *mp, int nsamp, uint8_t **txdata, int len);

void smi_setup(int channels, int ns, int setup, int strobe, int hold, MEM_MAP *mp, int nsamp,
               uint8_t **txdata) {
  map_periph(&smi_regs, (void *)SMI_BASE, PAGE_SIZE);
  map_periph(&clk_regs, (void *)CLK_BASE, PAGE_SIZE);

  int width = channels > 8 ? SMI_16_BITS : SMI_8_BITS;
  int i, divi = ns / 2;

  smi_cs = (SMI_CS_REG *)REG32(smi_regs, SMI_CS);
  smi_l = (SMI_L_REG *)REG32(smi_regs, SMI_L);
  smi_a = (SMI_A_REG *)REG32(smi_regs, SMI_A);
  smi_d = (SMI_D_REG *)REG32(smi_regs, SMI_D);
  smi_dmc = (SMI_DMC_REG *)REG32(smi_regs, SMI_DMC);
  smi_dsr = (SMI_DSR_REG *)REG32(smi_regs, SMI_DSR0);
  smi_dsw = (SMI_DSW_REG *)REG32(smi_regs, SMI_DSW0);
  smi_dcs = (SMI_DCS_REG *)REG32(smi_regs, SMI_DCS);
  smi_dca = (SMI_DCA_REG *)REG32(smi_regs, SMI_DCA);
  smi_dcd = (SMI_DCD_REG *)REG32(smi_regs, SMI_DCD);
  smi_cs->value = smi_l->value = smi_a->value = 0;
  smi_dsr->value = smi_dsw->value = smi_dcs->value = smi_dca->value = 0;
  if (*REG32(clk_regs, CLK_SMI_DIV) != divi << 12) {
    *REG32(clk_regs, CLK_SMI_CTL) = CLK_PASSWD | (1 << 5);
    usleep(10);
    while (*REG32(clk_regs, CLK_SMI_CTL) & (1 << 7))
      ;
    usleep(10);
    *REG32(clk_regs, CLK_SMI_DIV) = CLK_PASSWD | (divi << 12);
    usleep(10);
    *REG32(clk_regs, CLK_SMI_CTL) = CLK_PASSWD | 6 | (1 << 4);
    usleep(10);
    while ((*REG32(clk_regs, CLK_SMI_CTL) & (1 << 7)) == 0)
      ;
    usleep(100);
  }
  if (smi_cs->seterr)
    smi_cs->seterr = 1;
  smi_dsr->rsetup = smi_dsw->wsetup = setup;
  smi_dsr->rstrobe = smi_dsw->wstrobe = strobe;
  smi_dsr->rhold = smi_dsw->whold = hold;
  smi_dmc->panicr = smi_dmc->panicw = 8;
  smi_dmc->reqr = smi_dmc->reqw = REQUEST_THRESH;
  smi_dsr->rwidth = smi_dsw->wwidth = width;
  for (i = 0; i < channels; i++)
    gpio_mode(SMI_SD0_PIN + i, GPIO_ALT1);

  setup_smi_dma(mp, nsamp, txdata, width + 1);
}

void smi_close(int channels) {
  for (size_t i = 0; i < channels; ++i)
    gpio_mode(SMI_SD0_PIN + i, GPIO_IN);
  if (smi_regs.virt) {
    *REG32(smi_regs, SMI_CS) = 0;
  }
  stop_dma(DMA_CHAN_A);
  unmap_periph_mem(&clk_regs);
  unmap_periph_mem(&smi_regs);
  dma_close();
}

// Start SMI DMA transfers
void start_smi(MEM_MAP *mp) {
  DMA_CB *cbs = mp->virt;

  start_dma(mp, DMA_CHAN_A, &cbs[0], 0);
  smi_cs->start = 1;
}

// private

// Set up SMI transfers using DMA
void setup_smi_dma(MEM_MAP *mp, int nsamp, uint8_t **txdata, int len) {
  smi_dmc->dmaen = 1;
  smi_cs->enable = 1;
  smi_cs->clear = 1;
  smi_cs->pxldat = 1;
  smi_l->len = nsamp * len;
  smi_cs->write = 1;

  dma_setup(mp, DMA_CHAN_A, nsamp, txdata, len, REG_BUS_ADDR(smi_regs, SMI_D));
}