#include "daisy.h"

#include "tft.hpp"

using namespace daisy;


static tft_cmd init_cmds[] = {
	tft_cmd(0xEF, {0x03, 0x80, 0x02}),
	tft_cmd(0xCF, {0x00, 0xC1, 0x30}),
	tft_cmd(0xED, {0x64, 0x03, 0x12, 0x81}),
	tft_cmd(0xE8, {0x85, 0x00, 0x78}),
	tft_cmd(0xCB, {0x39, 0x2C, 0x00, 0x34, 0x02}),
	tft_cmd(0xF7, {0x20}),
	tft_cmd(0xEA, {0x00, 0x00}),
	tft_cmd(ILI9341_PWCTR1, {0x23}),       // Power control VRH[5:0]
	tft_cmd(ILI9341_PWCTR2, {0x10}),             // Power control SAP[2:0];BT[3:0]
	tft_cmd(ILI9341_VMCTR1, {0x3e, 0x28}),       // VCM control
	tft_cmd(ILI9341_VMCTR2, {0x86}),             // VCM control2
	tft_cmd(ILI9341_MADCTL, {0x08}),             // Memory Access Control
	tft_cmd(ILI9341_VSCRSADD, {0x00}),             // Vertical scroll zero
	tft_cmd(ILI9341_PIXFMT, {0x55}), // 16 bit pixel fmt
	tft_cmd(ILI9341_FRMCTR1, {0x00, 0x18}),
	tft_cmd(ILI9341_DFUNCTR, {0x08, 0x82, 0x27}), // Display Function Control
	tft_cmd(0xF2, {0x00}),                         // 3Gamma Function Disable
	tft_cmd(ILI9341_GAMMASET, {0x01}),             // Gamma curve selected
	tft_cmd(ILI9341_GMCTRP1, {0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, // Set Gamma
	    0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, 0x0E, 0x09, 0x00}),
	tft_cmd(ILI9341_GMCTRN1, {0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, // Set Gamma
	    0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, 0x31, 0x36, 0x0F}),
	tft_cmd(ILI9341_SLPOUT, 150),                // Exit Sleep
	tft_cmd(ILI9341_DISPON, 150),                // Display on
};

static tft_cmd RESET_CMD(ILI9341_SWRESET, 150);

int
tft_driver_ili9341::init(uint8_t *dma_buf, size_t dma_sz)
{

	AC_PRECOND(_async_stat == async_status::INIT);

	SpiHandle::Result res;

	dcx_pin = daisy::seed::D12; // Pin(PORTB, 8);
	dcx.Init(dcx_pin, GPIO::Mode::OUTPUT, GPIO::Pull::NOPULL,
	    GPIO::Speed::HIGH);

	spi_conf.datasize = 8;

	spi_conf.mode = SpiHandle::Config::Mode::MASTER;
	spi_conf.direction = SpiHandle::Config::Direction::TWO_LINES;

	spi_conf.clock_polarity = SpiHandle::Config::ClockPolarity::LOW;
	spi_conf.clock_phase = SpiHandle::Config::ClockPhase::ONE_EDGE;

	spi_conf.baud_prescaler = SpiHandle::Config::BaudPrescaler::PS_2;

	spi_conf.periph = SpiHandle::Config::Peripheral::SPI_1;
	spi_conf.nss = SpiHandle::Config::NSS::HARD_OUTPUT;

	spi_conf.pin_config.nss  = daisy::seed::D7; //Pin(PORTG, 10); // D7
	spi_conf.pin_config.sclk = daisy::seed::D8; //Pin(PORTG, 11); // D8
	spi_conf.pin_config.miso = daisy::seed::D9; // Pin(PORTB, 4); // D9
	spi_conf.pin_config.mosi = daisy::seed::D10; //Pin(PORTB, 5); // D10

	res = spi.Init(spi_conf);

	if (res == SpiHandle::Result::ERR)
		return (-1);

	daisy_hw.PrintLine("resetting tft");
	send_sync(&RESET_CMD);

	for (auto cmd : init_cmds) {
		daisy_hw.PrintLine("sending tft init command");
		send_sync(&cmd);
	}

	_async_dma_buf = dma_buf;
	_async_dma_sz = dma_sz;
	_async_curr_rd = _async_curr_wr = (tft_dma_cmd *)dma_buf;
	_async_id = 0;
	_async_stat = async_status::INIT;

	return (0);
}

void
tft_driver_ili9341::set_addr_window(uint16_t col_st, uint16_t width,
    uint16_t row_st, uint16_t height)
{
	uint16_t start, end;

	start = col_st; end = col_st + width;
	tft_cmd col_set(ILI9341_CASET,
	    { (uint8_t)((start >> 8) & 0xff), (uint8_t)(start & 0xff),
	      (uint8_t)((end >> 8)   & 0xff), (uint8_t)(end   & 0xff) });

	start = row_st; end = row_st + height;
	tft_cmd row_set(ILI9341_PASET,
	    { (uint8_t)((start >> 8) & 0xff), (uint8_t)(start & 0xff),
	      (uint8_t)((end >> 8)   & 0xff), (uint8_t)(end   & 0xff) });

	send_sync(&col_set);
	send_sync(&row_set);
}


void
tft_driver_ili9341::async_on_finish(async_on_finish_f cb, void *ctx)
{
	_async_cb_fun = cb;
	_async_cb_ctx = ctx;
}

int
tft_driver_ili9341::async_ready(void)
{
	async_status stat;

	stat = (async_status)_async_stat;
	switch (stat) {
	case async_status::INIT:
	case async_status::DONE:
		if (_async_cb_fun == NULL ||
		    _async_cb_ctx == NULL ||
		    _async_id < 0)
			return (-1);
		_async_stat = async_status::READY;

		__attribute__((fallthrough));
	case async_status::READY:
		return (0);
	default:
		break;
	}

	return (-1);
}

int
tft_driver_ili9341::dma_buf_reset(void)
{
	async_status stat;
	tft_dma_cmd *cmd;

	if (dma_buf_sz_tx_pend() > 0)
		return (-1);

	stat = _async_stat;
	switch (stat) {
	case async_status::INIT:
	case async_status::TX_NONE:
	case async_status::TX_ACTIVE:
		return (-1);
	default:
		break;
	}

	cmd = (tft_dma_cmd *)_async_dma_buf;
	cmd->size = 0;

	_async_curr_wr = cmd;
	_async_curr_rd = cmd;

	return (0);
}

int
tft_driver_ili9341::async_start(void)
{
	if (async_ready() == -1)
		return (-1);

	if (dma_buf_reset() == -1)
		return (-1);

	_async_wr = async_wr::START;
	_async_stat = async_status::TX_NONE;

	return (async_id());
}

int
tft_driver_ili9341::async_end(void)
{
	_async_wr = async_wr::END;
	async_id_next();

	async_flush();

	return (0);
}

tft_driver_ili9341::async_status
tft_driver_ili9341::async_stat(void)
{
	return (_async_stat);
}

void
tft_driver_ili9341::async_finalize(void)
{
	AC_PRECOND(_async_cb_fun != NULL && _async_cb_ctx != NULL);
	AC_PRECOND(_async_stat == async_status::TX_NONE ||
	    _async_stat == async_status::TX_INACTIVE ||
	    _async_stat == async_status::TX_ACTIVE);
	AC_PRECOND(_async_wr == async_wr::END);
	AC_PRECOND(dma_buf_sz_tx_pend() == 0);

	_async_stat = async_status::DONE;
	_async_cb_fun((void *)_async_cb_ctx);
}

void
tft_driver_ili9341::async_flush(void)
{
	async_status stat;
	async_wr wr;

	stat = _async_stat;
	wr = _async_wr;

	switch (stat) {
	case async_status::TX_NONE:
	case async_status::TX_INACTIVE:
		if (dma_buf_sz_tx_pend() > 0) {
			async_tx();
			return;
		}
		if (wr == async_wr::END) {
			async_finalize();
			return;
		}
		dma_buf_reset();
		break;
	default:
		break;
	}
}

#define TFT_ASYNC_TRY_TX_MIN	2048
void
tft_driver_ili9341::async_tx_try(void)
{
	if (dma_buf_sz_tx_pend() < (_async_dma_sz / 2))
		return;

	async_flush();
}

void
tft_driver_ili9341::async_tx(void)
{
	async_status stat;

	stat = _async_stat;

	AC_ASSERT(stat == async_status::TX_NONE ||
	    stat == async_status::TX_INACTIVE);
	AC_ASSERT(dma_buf_sz_tx_pend() > sizeof(tft_dma_cmd));
	AC_ASSERT(_async_curr_rd->size > 0);

	_async_stat = async_status::TX_ACTIVE;

	async_send_raw((const uint8_t *)_async_curr_rd->buf, 1,
	    async_tx_cmd_scb, async_tx_cmd_ecb, this);
}

/* ret < 0: if error
 * ret = 0: if success
 * ret > 0: need reentrant call, ret == async_id that should be used in next
 * call
 */
int
tft_driver_ili9341::async_hline_row_bg_fill(int id, tft_color *bg,
    tft_color *fg, uint16_t hline_idx_st, uint16_t hline_idx_end,
    uint16_t row_idx)
{
	int ret;

	if (id != _async_id)
		return (-1);

	ret = render_dma_buf_hline_row_bg_fill(bg, fg, hline_idx_st,
	    hline_idx_end, row_idx);
	if (ret < 0)
		return (ret);

	if (ret > 0)
		async_flush();
	async_tx_try();

	return (ret);
}


void
tft_driver_ili9341::async_tx_cmd_scb(void *ctx)
{
	tft_driver_ili9341 *tft;

	tft = (tft_driver_ili9341 *)ctx;

	AC_ASSERT(tft->_async_stat == async_status::TX_ACTIVE);

	tft->dcx_low();
}

void
tft_driver_ili9341::async_tx_cmd_ecb(void *ctx, SpiHandle::Result res)
{
	tft_driver_ili9341 *tft;
	const tft_dma_cmd *cmd;

	tft = (tft_driver_ili9341 *)ctx;
	cmd = tft->dma_buf_curr_rd();

	AC_ASSERT(tft->_async_stat == async_status::TX_ACTIVE);

	tft->async_send_raw(&cmd->buf[1], cmd->size, async_tx_param_scb,
	    async_tx_param_ecb, tft);
}

void
tft_driver_ili9341::async_tx_param_scb(void *ctx)
{
	tft_driver_ili9341 *tft;

	tft = (tft_driver_ili9341 *)ctx;

	AC_ASSERT(tft->_async_stat == async_status::TX_ACTIVE);

	tft->dcx_high();
}

void
tft_driver_ili9341::async_tx_param_ecb(void *ctx, SpiHandle::Result res)
{
	tft_driver_ili9341 *tft;
	const tft_dma_cmd *cmd;


	tft = (tft_driver_ili9341 *)ctx;

	AC_PRECOND(tft->_async_stat == async_status::TX_ACTIVE);

	cmd = tft->dma_buf_next_rd();

	if (cmd->size == 0) {
		if (tft->_async_wr == async_wr::END) {
			AC_ASSERT(tft->dma_buf_sz_tx_pend() == 0);
			tft->async_finalize();
		} else {
			tft->_async_stat = async_status::TX_INACTIVE;
		}
		return;
	}

	AC_ASSERT(tft->dma_buf_sz_tx_pend() > sizeof(tft_dma_cmd));

	tft->async_send_raw(cmd->buf, 1, async_tx_cmd_scb, async_tx_cmd_ecb,
	    tft);
}

size_t
tft_driver_ili9341::dma_buf_sz_tx_pend(void)
{
	return ((size_t)
	    ((uint8_t *)_async_curr_wr - (uint8_t *)_async_curr_rd));
}

size_t
tft_driver_ili9341::dma_buf_sz_used(void)
{
	size_t buf_fill;

	buf_fill = (size_t)((uint8_t *)_async_curr_wr - _async_dma_buf) +
	    sizeof(tft_dma_cmd);

	return (buf_fill);
}

tft_dma_cmd *
tft_driver_ili9341::dma_buf_curr_wr(void)
{
	return ((tft_dma_cmd *)_async_curr_wr);
}

tft_dma_cmd *
tft_driver_ili9341::dma_buf_next_wr(size_t curr_sz)
{
	size_t tot_sz;
	volatile tft_dma_cmd *curr, *next;
	volatile uint8_t *bytes;

	AC_PRECOND(_async_dma_sz - dma_buf_sz_used() >=
	    curr_sz + sizeof(tft_dma_cmd));

	curr = (volatile tft_dma_cmd *)_async_curr_wr;
	tot_sz = sizeof(*curr) + curr_sz;

	bytes = (volatile uint8_t *)curr;
	bytes += tot_sz;

	next = (volatile tft_dma_cmd *)bytes;
	next->size = 0;

	curr->size = curr_sz;

	_async_curr_wr = next;
	return ((tft_dma_cmd *)next);
}

const tft_dma_cmd *
tft_driver_ili9341::dma_buf_curr_rd(void)
{
	return ((const tft_dma_cmd *)_async_curr_rd);
}

const tft_dma_cmd *
tft_driver_ili9341::dma_buf_next_rd(void)
{
	size_t sz;
	volatile const tft_dma_cmd *cmd;
	volatile const uint8_t *bytes;


	cmd = (volatile const tft_dma_cmd *)_async_curr_rd;
	sz = sizeof(*cmd) + cmd->size;

	bytes = (volatile const uint8_t *)cmd;
	bytes += sz;

	cmd = (volatile const tft_dma_cmd *)bytes;
	_async_curr_rd = cmd;

	return ((const tft_dma_cmd *)cmd);
}



#define SET_ADDR_WINDOW_SZ	(sizeof(tft_dma_cmd) + 5 + \
				 sizeof(tft_dma_cmd) + 5)
int
tft_driver_ili9341::render_dma_buf_set_addr_window(uint16_t col_st,
    uint16_t col_end, uint16_t row_st, uint16_t row_end)
{
	size_t n;
	tft_dma_cmd *cmd;

	if (_async_dma_sz - dma_buf_sz_used() < SET_ADDR_WINDOW_SZ)
		return (1);

	cmd = dma_buf_curr_wr();

	n = 0;
	cmd->buf[n++] = ILI9341_CASET;
	cmd->buf[n++] = (uint8_t)((col_st >> 8) & 0xff);
	cmd->buf[n++] = (uint8_t)(col_st & 0xff);
	cmd->buf[n++] = (uint8_t)((col_end >> 8) & 0xff);
	cmd->buf[n++] = (uint8_t)(col_end & 0xff);

	cmd = dma_buf_next_wr(n);

	n = 0;
	cmd->buf[n++] = ILI9341_PASET;
	cmd->buf[n++] = (uint8_t)((row_st >> 8) & 0xff);
	cmd->buf[n++] = (uint8_t)(row_st & 0xff);
	cmd->buf[n++] = (uint8_t)((row_end >> 8) & 0xff);
	cmd->buf[n++] = (uint8_t)(row_end & 0xff);

	dma_buf_next_wr(n);

	return (0);
}

#define MEMWR_SZ(num)	(sizeof(tft_dma_cmd) + 1 + (num) * sizeof(uint16_t))
int
tft_driver_ili9341::render_dma_buf_memwr(const tft_color *col, size_t num)
{
	size_t i, n;
	uint16_t c16;
	tft_dma_cmd *cmd;

	if (_async_dma_sz - dma_buf_sz_used() < MEMWR_SZ(num))
		return (1);

	cmd = dma_buf_curr_wr();

	c16 = col->u16();
	i = 0;
	cmd->buf[i++] = ILI9341_RAMWR;
	for (n = 0; n < num; n++) {
		cmd->buf[i++] = (uint8_t)((c16 >> 8) & 0xff);
		cmd->buf[i++] = (uint8_t)(c16 & 0xff);
	}

	dma_buf_next_wr(i);

	return (0);
}

#define MEMWR_CONT_SZ(num)	MEMWR_SZ(num)
int
tft_driver_ili9341::render_dma_buf_memwr_cont(const tft_color *col, size_t num)
{
	size_t i, n;
	uint16_t c16;
	tft_dma_cmd *cmd;

	if (_async_dma_sz - dma_buf_sz_used() < MEMWR_CONT_SZ(num)) {
		return (1);
	}

	cmd = dma_buf_curr_wr();

	c16 = col->u16();
	i = 0;
	cmd->buf[i++] = ILI9341_WRCONT;
	for (n = 0; n < num; n++) {
		cmd->buf[i++] = (uint8_t)((c16 >> 8) & 0xff);
		cmd->buf[i++] = (uint8_t)(c16 & 0xff);
	}

	dma_buf_next_wr(i);

	return (0);
}


int
tft_driver_ili9341::render_dma_buf_hline(const tft_color *c, uint16_t col_s,
    uint16_t col_e, uint16_t row)
{
	int ret;

	AC_PRECOND(col_s <= col_e);

	if (_async_dma_sz - dma_buf_sz_used() <
	    SET_ADDR_WINDOW_SZ + MEMWR_SZ(col_e - col_s))
		return (1);

	ret = render_dma_buf_set_addr_window(col_s, col_e, row, row + 1);
	if (ret != 0)
		return (ret);

	ret = render_dma_buf_memwr(c, (size_t)(col_e - col_s));

	return (ret);
}

int
tft_driver_ili9341::render_dma_buf_hline_row_bg_fill(const tft_color *bg,
    const tft_color *fg, uint16_t col_s, uint16_t col_e, uint16_t row)
{
	int ret;

	AC_PRECOND(col_s <= col_s && col_e <= width());

	if (_async_dma_sz - dma_buf_sz_used() <
	    SET_ADDR_WINDOW_SZ + MEMWR_SZ(col_s) + MEMWR_CONT_SZ(col_e - col_s))
		return (1);

	ret = render_dma_buf_set_addr_window(0, width(), row, row+1);
	if (ret != 0)
		return (ret);

	ret = render_dma_buf_memwr(bg, (size_t)col_s);
	if (ret != 0)
		return (ret);

	ret = render_dma_buf_memwr_cont(fg, (size_t)(col_e - col_s));
	if (ret != 0)
		return (ret);

	ret = render_dma_buf_memwr_cont(bg, (size_t)(width() - col_e));

	return (ret);
}

#define TFT_N_PIXELS		(ILI9341_TFTWIDTH * ILI9341_TFTHEIGHT)
#define TFT_BLOCK_SZ		((size_t)16386)
int
tft_driver_ili9341::fill_screen(const tft_color *c)
{
	uint8_t buf[TFT_BLOCK_SZ];
	size_t	n, i;
	uint16_t c16;

	set_addr_window(0, width(), 0, height());
	if (send_sync(ILI9341_RAMWR, NULL, 0) == -1)
		return (-1);

	i = 0;
	c16 = c->u16();
	for (n = 0; n < sizeof(buf) / sizeof(uint16_t); n++) {
		buf[i++] = (uint8_t)((c16 >> 8) & 0xff);
		buf[i++] = (uint8_t)(c16 & 0xff);
	}

	for (n = 0;
	    n < TFT_N_PIXELS * sizeof(uint16_t);
	    n += i) {
		if (send_raw_sync(buf,
		    std::min(i, (TFT_N_PIXELS * sizeof(uint16_t)) - n)) == -1)
			return (-1);
	}

	return (0);
}

int
tft_driver_ili9341::send_sync(const tft_cmd *cmd)
{
	if (send_sync(cmd->cmd, cmd->param_bytes(), cmd->param_len) == -1)
		return (-1);

	if (cmd->post_delay > 0)
		System::Delay(cmd->post_delay);

	return (0);
}

int
tft_driver_ili9341::send_sync(uint8_t cmd, const uint8_t *buf, size_t len)
{
	SpiHandle::Result res;

	dcx_low();
	res = spi.BlockingTransmit(&cmd, 1);
	dcx_high();

	if (res == SpiHandle::Result::ERR)
		return (-1);

	if (len == 0)
		return (0);

	res = spi.BlockingTransmit((uint8_t *)buf, len);

	return (res == SpiHandle::Result::ERR ? -1 : 0);
}

int
tft_driver_ili9341::send_raw_sync(const uint8_t *buf, size_t len)
{
	size_t block_st, transfer_sz;
	SpiHandle::Result res;

	res = SpiHandle::Result::OK;
	for (block_st = 0; block_st < len; block_st += TFT_BLOCK_SZ) {
		transfer_sz = std::min(TFT_BLOCK_SZ, len - block_st);
		res = spi.BlockingTransmit((uint8_t *)&buf[block_st],
		    transfer_sz);
	}

	return (res == SpiHandle::Result::ERR ? -1 : 0);
}

