#include "display.h"

#include "devicelib.h"
#include "pixelstream.h"

#include <hardware/dma.h>
#include <hardware/irq.h>
#include <hardware/spi.h>
#include <pico/stdlib.h>

using namespace lib;

namespace
{

constexpr uint PIXELS_PER_TRANSFER = 8;

int dma = -1;
uint8_t cs;
volatile int transfers_left = 0;
volatile uint8_t color_buffer[3 * PIXELS_PER_TRANSFER];

void dma_handler()
{
  dma_hw->ints0 = 1u << dma;
  transfers_left -= PIXELS_PER_TRANSFER;

  dma_channel_set_read_addr(dma, color_buffer, transfers_left > 0);

  gpio_put(cs, transfers_left <= 0);
  busy_wait_at_least_cycles(35);
}

}

Display::Display(uint8_t sck, uint8_t tx, uint8_t cs, uint8_t dc,
                 int baudrate) :
  sck(sck),
  tx(tx),
  cs(cs),
  dc(dc),
  frame(false)
{
  spi_inst_t* sck_spi = detail::get_spi_instance(sck);
  spi_inst_t* tx_spi = detail::get_spi_instance(tx);

  hard_assert(sck_spi == tx_spi, "Invalid configuration");
  spi = sck_spi;

  // only support one display with DMA for now
  if (::dma < 0)
  {
    dma = dma_claim_unused_channel(false);
  }
  else
  {
    dma = -1;
  }

  if (dma >= 0)
  {
    ::dma = dma;
    ::cs = cs;

    dma_channel_config config = dma_channel_get_default_config(dma);
    channel_config_set_transfer_data_size(&config, DMA_SIZE_8);
    channel_config_set_read_increment(&config, true);
    channel_config_set_write_increment(&config, false);
    channel_config_set_dreq(&config, spi_get_dreq(spi, true));

    dma_channel_configure(dma, &config, &spi_get_hw(spi)->dr, color_buffer,
                          3 * PIXELS_PER_TRANSFER, false);

    dma_channel_set_irq0_enabled(dma, true);

    irq_set_exclusive_handler(DMA_IRQ_0, dma_handler);
    irq_set_enabled(DMA_IRQ_0, true);
  }

  gpio_set_function(sck, GPIO_FUNC_SPI);
  gpio_set_function(tx, GPIO_FUNC_SPI);

  // chip select and data/command controlled manually
  gpio_set_function(cs, GPIO_FUNC_SIO);
  gpio_set_dir(cs, GPIO_OUT);
  gpio_set_function(dc, GPIO_FUNC_SIO);
  gpio_set_dir(dc, GPIO_OUT);

  spi_init(spi, baudrate);
  spi_set_format(spi, 8, SPI_CPOL_1, SPI_CPHA_1, SPI_MSB_FIRST);

  // initialize display command sequence
  // -----------------------------------
  // reset device
  send_command(0x01);
  sleep_ms(150);

  // take device out of sleep mode
  send_command(0x11);
  sleep_ms(10);

  // set display format to 18-bit color
  send_command(0x3A, 0x66);
  sleep_ms(10);

  // set memory addressing mode
  send_command(0x36, 0b11000000);

  // turn on display inversion
  send_command(0x21);

  // set display to normal mode
  send_command(0x13);
  sleep_ms(10);

  // set display brightness
  send_command(0x51, 0x00);

  // turn on display
  send_command(0x29);
  sleep_ms(10);

  clear();
  sleep_ms(25);

  // set framerate
  send_command(0xC6, 0x00);
  {
    constexpr uint8_t params[5] = { 0x01, 0x01, 0x01, 0x11, 0x11 };
    send_command(0xB2, params, 5);
  }
}

void Display::set_update_area(uint16_t left, uint16_t top, uint16_t width,
                              uint16_t height)
{
  const uint16_t right = left + width - 1;
  const uint16_t bottom = top + height - 1;
  {
    uint8_t params[4]
      = { static_cast<uint8_t>(left >> 8), static_cast<uint8_t>(left & 0xFF),
          static_cast<uint8_t>(right >> 8),
          static_cast<uint8_t>(right & 0xFF) };

    send_command(0x2A, params, 4);
  }
  {
    uint8_t params[4]
      = { static_cast<uint8_t>(top >> 8), static_cast<uint8_t>(top & 0xFF),
          static_cast<uint8_t>(bottom >> 8),
          static_cast<uint8_t>(bottom & 0xFF) };

    send_command(0x2B, params, 4);
  }
}

void Display::fill(uint16_t left, uint16_t top, uint16_t width,
                   uint16_t height, uint8_t r, uint8_t g, uint8_t b)
{
  if (dma >= 0)
  {
    while (transfers_left > 0)
    {
      dma_channel_wait_for_finish_blocking(dma);
    }

    for (int i = 0; i < PIXELS_PER_TRANSFER; i++)
    {
      color_buffer[3 * i + 0] = r;
      color_buffer[3 * i + 1] = g;
      color_buffer[3 * i + 2] = b;
    }

    set_update_area(left, top, width, height);
    begin_dma(color_buffer, 3 * PIXELS_PER_TRANSFER, width * height);
  }
  // fallback if DMA is not available
  else
  {
    set_update_area(left, top, width, height);

    auto pixels = this->pixels();
    for (int i = 0; i < width * height; i++)
    {
      pixels.write(r, g, b);
    }
  }
}

void Display::fill(uint16_t left, uint16_t top, uint16_t width,
                   uint16_t height, const uint8_t* src)
{
  if (dma >= 0)
  {
    while (transfers_left > 0)
    {
      dma_channel_wait_for_finish_blocking(dma);
    }

    set_update_area(left, top, width, height);
    // pixel count is set to 1 since this is the whole buffer
    begin_dma(src, width * height * 3, 1);
  }
  // fallback if DMA is not available
  else
  {
    set_update_area(left, top, width, height);

    pixels().write(src, width * height * 3);
  }
}

PixelStream Display::pixels() { return PixelStream(*this); }

void Display::clear() { fill(0, 0, 240, 320, 0, 0, 0); }

void Display::begin_dma(const volatile uint8_t* src, uint32_t transfer_count,
                        uint32_t pixel_count)
{
  gpio_put(cs, false);
  send_command_and_begin_data_stream(0x2C);
  transfers_left = pixel_count;
  dma_channel_transfer_from_buffer_now(dma, src, transfer_count);
}

void Display::send_command(uint8_t command)
{
  send_command(command, nullptr, 0);
}

void Display::send_command(uint8_t command, uint8_t param)
{
  send_command(command, &param, 1);
}

void Display::send_command(uint8_t command, const uint8_t* params,
                           size_t param_count)
{
  gpio_put(cs, false);

  gpio_put(dc, false);
  spi_write_blocking(spi, &command, 1);

  if (param_count > 0)
  {
    // parameters are sent via data mode
    gpio_put(dc, true);
    spi_write_blocking(spi, params, param_count);
  }

  gpio_put(cs, true);
}

void Display::send_command_and_begin_data_stream(uint8_t command)
{
  gpio_put(cs, false);

  gpio_put(dc, false);
  spi_write_blocking(spi, &command, 1);

  gpio_put(dc, true);
}

void Display::send_data(uint8_t data) { spi_write_blocking(spi, &data, 1); }

void Display::send_data(const uint8_t* data, size_t datalen)
{
  spi_write_blocking(spi, data, datalen);
}

void Display::end_data_stream() { gpio_put(cs, true); }
