#include "hardware/dma.h"
#include "hardware/irq.h"
#include "common/pimoroni_common.hpp"

#include "pico_unicorn.pio.h"
#include "pico_unicorn.hpp"

// pixel data is stored as a stream of bits delivered in the
// order the PIO needs to manage the shift registers, row
// selects, delays, and latching/blanking
//
// the data consists of 7 rows each of which has 14 frames of
// bcd timing data
//
// each row looks like this:
//
//      0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, # pixel data
//      0b00000000, # dummy byte to 32-bit align the frame (could be used to extend row select in future)
//      0b01111111, # row 0 select (7-bit row address, 1-bit dummy data)
//      0b00001111, 0b11111111, # bcd tick count (0-65536)
//
//      .. next BCD frame for this row (repeat for 14 frames)
//
//  .. next row (repeat for 7 rows)
//
// pixels are encoded as 4 bits: r, g, b, dummy to conveniently
// pack them into nibbles

enum pin {
  LED_DATA  = 8,
  LED_CLOCK = 9,
  LED_LATCH = 10,
  LED_BLANK = 11,
  ROW_0 = 22,
  ROW_1 = 21,
  ROW_2 = 20,
  ROW_3 = 19,
  ROW_4 = 18,
  ROW_5 = 17,
  ROW_6 = 16,
  A     = 12,
  B     = 13,
  X     = 14,
  Y     = 15,
};

constexpr uint32_t ROW_COUNT = 7;
constexpr uint32_t ROW_BYTES = 12;
constexpr uint32_t BCD_FRAMES = 15; // includes fet discharge frame
constexpr uint32_t BITSTREAM_LENGTH = (ROW_COUNT * ROW_BYTES * BCD_FRAMES);

// must be aligned for 32bit dma transfer
alignas(4) static uint8_t bitstream[BITSTREAM_LENGTH] = {0};

static uint32_t dma_channel;

static inline void unicorn_jetpack_program_init(PIO pio, uint sm, uint offset) {
  pio_gpio_init(pio, pin::LED_DATA);
  pio_gpio_init(pio, pin::LED_CLOCK);
  pio_gpio_init(pio, pin::LED_LATCH);
  pio_gpio_init(pio, pin::LED_BLANK);
  pio_gpio_init(pio, pin::ROW_0);
  pio_gpio_init(pio, pin::ROW_1);
  pio_gpio_init(pio, pin::ROW_2);
  pio_gpio_init(pio, pin::ROW_3);
  pio_gpio_init(pio, pin::ROW_4);
  pio_gpio_init(pio, pin::ROW_5);
  pio_gpio_init(pio, pin::ROW_6);

  pio_sm_set_consecutive_pindirs(pio, sm, pin::LED_DATA, 4, true);
  pio_sm_set_consecutive_pindirs(pio, sm, pin::ROW_6, 7, true);

  pio_sm_config c = unicorn_program_get_default_config(offset);

  // osr shifts right, autopull on, autopull threshold 8
  sm_config_set_out_shift(&c, true, false, 32);

  // configure out, set, and sideset pins
  sm_config_set_out_pins(&c, pin::ROW_6, 7);
  sm_config_set_sideset_pins(&c, pin::LED_CLOCK);
  sm_config_set_set_pins(&c, pin::LED_DATA, 4);

  // join fifos as only tx needed (gives 8 deep fifo instead of 4)
  sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);

  pio_sm_init(pio, sm, offset, &c);
  pio_sm_set_enabled(pio, sm, true);
}

namespace pimoroni {

  // once the dma transfer of the scanline is complete we move to the
  // next scanline (or quit if we're finished)
  void __isr dma_complete() {
    if (dma_hw->ints0 & (1u << dma_channel)) {
      dma_hw->ints0 = (1u << dma_channel); // clear irq flag
      dma_channel_set_trans_count(dma_channel, BITSTREAM_LENGTH / 4, false);
      dma_channel_set_read_addr(dma_channel, bitstream, true);
    }
  }

  PicoUnicorn::~PicoUnicorn() {
    // stop and release the dma channel
    irq_set_enabled(DMA_IRQ_0, false);
    dma_channel_set_irq0_enabled(dma_channel, false);
    irq_set_enabled(pio_get_dreq(bitstream_pio, bitstream_sm, true), false);
    irq_remove_handler(DMA_IRQ_0, dma_complete);

    dma_channel_wait_for_finish_blocking(dma_channel);
    dma_channel_unclaim(dma_channel);

    // release the pio and sm
    pio_sm_unclaim(bitstream_pio, bitstream_sm);
    pio_clear_instruction_memory(bitstream_pio);
    pio_sm_restart(bitstream_pio, bitstream_sm);
  }

  void PicoUnicorn::init() {
    // todo: shouldn't need to do this if things were cleaned up properly but without
    // this any attempt to run a micropython script twice will fail
    static bool already_init = false;

    // setup pins
    gpio_init(pin::LED_DATA); gpio_set_dir(pin::LED_DATA, GPIO_OUT);
    gpio_init(pin::LED_CLOCK); gpio_set_dir(pin::LED_CLOCK, GPIO_OUT);
    gpio_init(pin::LED_LATCH); gpio_set_dir(pin::LED_LATCH, GPIO_OUT);
    gpio_init(pin::LED_BLANK); gpio_set_dir(pin::LED_BLANK, GPIO_OUT);

    gpio_init(pin::ROW_0); gpio_set_dir(pin::ROW_0, GPIO_OUT);
    gpio_init(pin::ROW_1); gpio_set_dir(pin::ROW_1, GPIO_OUT);
    gpio_init(pin::ROW_2); gpio_set_dir(pin::ROW_2, GPIO_OUT);
    gpio_init(pin::ROW_3); gpio_set_dir(pin::ROW_3, GPIO_OUT);
    gpio_init(pin::ROW_4); gpio_set_dir(pin::ROW_4, GPIO_OUT);
    gpio_init(pin::ROW_5); gpio_set_dir(pin::ROW_5, GPIO_OUT);
    gpio_init(pin::ROW_6); gpio_set_dir(pin::ROW_6, GPIO_OUT);

    // initialise the bcd timing values and row selects in the bitstream
    for(uint8_t row = 0; row < HEIGHT; row++) {
      for(uint8_t frame = 0; frame < BCD_FRAMES; frame++) {
        // determine offset in the buffer for this row/frame
        uint16_t offset = (row * ROW_BYTES * BCD_FRAMES) + (ROW_BYTES * frame);

        uint16_t row_select_offset = offset + 9;
        uint16_t bcd_offset = offset + 10;

        // the last bcd frame is used to allow the fets to discharge to avoid ghosting
        if(frame == BCD_FRAMES - 1) {
          bitstream[row_select_offset] = 0b11111111;

          uint16_t bcd_ticks = 65535;
          bitstream[bcd_offset + 1] = (bcd_ticks &  0xff00) >> 8;
          bitstream[bcd_offset]     = (bcd_ticks &  0xff);

          for(uint8_t col = 0; col < 6; col++) {
            bitstream[offset + col] = 0xff;
          }
        }else{
          uint8_t row_select_mask = ~(1 << (7 - row));
          bitstream[row_select_offset] = row_select_mask;

          uint16_t bcd_ticks = 1 << frame;
          bitstream[bcd_offset + 1] = (bcd_ticks &  0xff00) >> 8;
          bitstream[bcd_offset]     = (bcd_ticks &  0xff);
        }
      }
    }

    // setup button inputs
    gpio_set_function(pin::A, GPIO_FUNC_SIO); gpio_set_dir(pin::A, GPIO_IN); gpio_pull_up(pin::A);
    gpio_set_function(pin::B, GPIO_FUNC_SIO); gpio_set_dir(pin::B, GPIO_IN); gpio_pull_up(pin::B);
    gpio_set_function(pin::X, GPIO_FUNC_SIO); gpio_set_dir(pin::X, GPIO_IN); gpio_pull_up(pin::X);
    gpio_set_function(pin::Y, GPIO_FUNC_SIO); gpio_set_dir(pin::Y, GPIO_IN); gpio_pull_up(pin::Y);

    if(already_init) {
      // stop and release the dma channel
      irq_set_enabled(DMA_IRQ_0, false);
      dma_channel_abort(dma_channel);
      dma_channel_wait_for_finish_blocking(dma_channel);

      dma_channel_set_irq0_enabled(dma_channel, false);
      irq_set_enabled(pio_get_dreq(bitstream_pio, bitstream_sm, true), false);
      irq_remove_handler(DMA_IRQ_0, dma_complete);

      dma_channel_unclaim(dma_channel);

      // release the pio and sm
      pio_sm_unclaim(bitstream_pio, bitstream_sm);
      pio_clear_instruction_memory(bitstream_pio);
      pio_sm_restart(bitstream_pio, bitstream_sm);
      //return;
    }

    // setup the pio
    bitstream_pio = pio0;
    bitstream_sm = pio_claim_unused_sm(pio0, true);
    sm_offset = pio_add_program(bitstream_pio, &unicorn_program);
    unicorn_jetpack_program_init(bitstream_pio, bitstream_sm, sm_offset);

    // setup dma transfer for pixel data to the pio
    dma_channel = dma_claim_unused_channel(true);
    dma_channel_config config = dma_channel_get_default_config(dma_channel);
    channel_config_set_transfer_data_size(&config, DMA_SIZE_32);
    channel_config_set_bswap(&config, false); // byte swap to reverse little endian
    channel_config_set_dreq(&config, pio_get_dreq(bitstream_pio, bitstream_sm, true));
    dma_channel_configure(dma_channel, &config, &bitstream_pio->txf[bitstream_sm], NULL, 0, false);
    dma_channel_set_irq0_enabled(dma_channel, true);
    irq_set_enabled(pio_get_dreq(bitstream_pio, bitstream_sm, true), true);
    irq_set_exclusive_handler(DMA_IRQ_0, dma_complete);
    irq_set_enabled(DMA_IRQ_0, true);

    dma_channel_set_trans_count(dma_channel, BITSTREAM_LENGTH / 4, false);
    dma_channel_set_read_addr(dma_channel, bitstream, true);

    already_init = true;
  }

  void PicoUnicorn::clear() {
    for(uint8_t y = 0; y < HEIGHT; y++) {
      for(uint8_t x = 0; x < WIDTH; x++) {
        set_pixel(x, y, 0);
      }
    }
  }

  void PicoUnicorn::set_pixel(uint8_t x, uint8_t y, uint8_t r, uint8_t g, uint8_t b) {
    if(x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) return;

    // make those coordinates sane
    x = (WIDTH - 1) - x;

    // work out the byte offset of this pixel
    uint8_t byte_offset = x / 2;

    // check if it's the high or low nibble and create mask and shift value
    uint8_t shift = x % 2 == 0 ? 0 : 4;
    uint8_t nibble_mask = 0b00001111 << shift;

    uint16_t gr = pimoroni::GAMMA_14BIT[r];
    uint16_t gg = pimoroni::GAMMA_14BIT[g];
    uint16_t gb = pimoroni::GAMMA_14BIT[b];

    // set the appropriate bits in the separate bcd frames
    for(uint8_t frame = 0; frame < BCD_FRAMES; frame++) {
      // determine offset in the buffer for this row/frame
      uint16_t offset = (y * ROW_BYTES * BCD_FRAMES) + (ROW_BYTES * frame);

      uint8_t rgbd = ((gr & 0b1) << 1) | ((gg & 0b1) << 3) | ((gb & 0b1) << 2);

      // shift to correct nibble
      rgbd <<= shift;

      // clear existing data
      bitstream[offset + byte_offset] &= ~nibble_mask;

      // set new data
      bitstream[offset + byte_offset] |= rgbd;

      gr >>= 1;
      gg >>= 1;
      gb >>= 1;
    }
  }

  void PicoUnicorn::set_pixel(uint8_t x, uint8_t y, uint8_t v) {
    set_pixel(x, y, v, v, v);
  }

  bool PicoUnicorn::is_pressed(uint8_t button) {
    return !gpio_get(button);
  }

}
