/** 
 * \file lcd.c
 * \brief LCD ST7920 source file
 * \details File contains functions definitions for LCD controller ST7920 (one master, one slave/LCD)
 *
 * \author Juraj Golej
 * \version 1.0
 */

#include "lcd.h"

struct LCD_app {
	struct SPI_module spi;
	struct TIMER_module timer;

	volatile enum LCD_MODE mode;
	volatile bool isBusy;

	uint16_t dataArray[LCD_DATA_BUFFSIZE];
	volatile struct circbuff data;
};

static struct LCD_app st7920;

static void _LCD_delay_end(struct TIMER_module* const module);

static void _LCD_spi_complete_transfer(struct SPI_module* const module, volatile const uint16_t* buffer, const uint16_t len);

static void _LCD_transfer();

static void _LCD_push_buffer(enum LCD_RS aRS, uint8_t aData);
 
enum retCode LCD_init()
{
	/* Configure SPI module */
	SPI_init(&st7920.spi, LCD_SPI);
	Try(SPI_configureChannel(&st7920.spi, LCD_SPI_CHANNEL, LCD_SPI_SPCK, LCD_SPI_MODE, LCD_SPI_BITS), EInvalidParameter);

	/* Configure TIMER module */
	Try(TIMER_init(&st7920.timer, LCD_TIMER, LCD_TIMER_CHANNEL, LCD_TIMER_CLK, LCD_EXEC_TIME), EInvalidParameter);
	TIMER_registerCallback(&st7920.timer, _LCD_delay_end);

	/* Configure another variables */
	st7920.mode = LCD_MODE_TEXT;
	st7920.isBusy = false;
	Try(circbuff_init(&st7920.data, &st7920.dataArray, sizeof(uint16_t), LCD_DATA_BUFFSIZE), EInvalidParameter);

	_LCD_push_buffer(LCD_RS_INSTRUCTION, LCD_CMD_TXT);
	_LCD_push_buffer(LCD_RS_INSTRUCTION, LCD_CMD_TXT);
	_LCD_push_buffer(LCD_RS_INSTRUCTION, LCD_CMD_TXT_MOVR);
	_LCD_push_buffer(LCD_RS_INSTRUCTION, LCD_CMD_TXT_CLEAR);
	_LCD_push_buffer(LCD_RS_INSTRUCTION, LCD_CMD_TXT_DISPON);

	return ENone;
}

enum retCode LCD_home()
{
	Try(st7920.mode != LCD_MODE_TEXT, EDenied);
	_LCD_push_buffer(LCD_RS_INSTRUCTION, LCD_CMD_TXT_HOME);

	return ENone;
}

void LCD_clear()
{
	if(st7920.mode == LCD_MODE_TEXT) {
		_LCD_push_buffer(LCD_RS_INSTRUCTION, LCD_CMD_TXT_CLEAR);
		_LCD_push_buffer(LCD_RS_INSTRUCTION, LCD_CMD_TXT_HOME);
	}
	else {
		LCD_show_image(NULL);
	}
}

enum retCode LCD_goto(const uint8_t aRow, const uint8_t aColumn)
{
	Try(st7920.mode != LCD_MODE_TEXT, EDenied);

	uint8_t address = aColumn;
	switch(aRow) {
		case 0:
			address += 0x80;
			break;
		case 1:
			address += 0x90;
			break;
		case 2:
			address += 0x88;
			break;
		case 3:
			address += 0x98;
			break;
		default:
			address += 0x80;
			break;
	}

	_LCD_push_buffer(LCD_RS_INSTRUCTION, address);

	return ENone;
}
 
enum retCode LCD_putc(const uint8_t aData)
{	
	Try(st7920.mode != LCD_MODE_TEXT, EDenied);
	_LCD_push_buffer(LCD_RS_DATA, aData);
	_LCD_push_buffer(LCD_RS_DATA, ' ');

	return ENone;
}
 
enum retCode LCD_puts(const uint8_t* aData)
{
	Try(st7920.mode != LCD_MODE_TEXT, EDenied);

	while(*aData) {
		_LCD_push_buffer(LCD_RS_DATA, *aData++);
	}

	return ENone;
}

enum retCode LCD_setGraphicMode()
{
	Try(st7920.mode != LCD_MODE_TEXT, EDenied);
	_LCD_push_buffer(LCD_RS_INSTRUCTION, LCD_CMD_IMG);
	st7920.mode = LCD_MODE_IMAGE;

	return ENone;
}

enum retCode LCD_setTextMode()
{
	Try(st7920.mode != LCD_MODE_IMAGE, EDenied);
	_LCD_push_buffer(LCD_RS_INSTRUCTION, LCD_CMD_TXT);
	st7920.mode = LCD_MODE_TEXT;

	return ENone;
}

enum retCode LCD_show_image(const uint8_t* aImage)
{
	Try(st7920.mode != LCD_MODE_IMAGE, EDenied);
	_LCD_push_buffer(LCD_RS_INSTRUCTION, LCD_CMD_IMG_DISPON);

	uint16_t m = 0;
	for(uint8_t w = 0; w <= 8; w += 8) {
		for(uint8_t i = 0; i < 32; ++i) {
			_LCD_push_buffer(LCD_RS_INSTRUCTION, 0x80 | i);
			_LCD_push_buffer(LCD_RS_INSTRUCTION, 0x80 | w);
			for(uint8_t z = 0; z < 16; ++z, ++m) {
				if(aImage) {
					_LCD_push_buffer(LCD_RS_DATA, aImage[m]);
				}
				else {
					_LCD_push_buffer(LCD_RS_DATA, 0);
				}
			}
		}
	}

	return ENone;
}

static void _LCD_delay_end(struct TIMER_module* const module)
{
	_LCD_transfer();
}

static void _LCD_spi_complete_transfer(struct SPI_module* const module, volatile const uint16_t* buffer, const uint16_t len)
{
	if((buffer[1] == (LCD_CMD_TXT_CLEAR & 0xF0)) && (buffer[2] == ((LCD_CMD_TXT_CLEAR << 4) & 0xF0))) {
		TIMER_setInterval(&st7920.timer, LCD_EXEC_TIME_CLRI);
	}
	else {
		TIMER_setInterval(&st7920.timer, LCD_EXEC_TIME);
	}

	TIMER_start(&st7920.timer, TIMER_MODE_SINGLE);
}

static void _LCD_transfer()
{
	if(circbuff_isEmpty(&st7920.data)) {
		st7920.isBusy = false;
		return;
	}
	else {
		st7920.isBusy = true;
	}

	static uint16_t bytes[3];
	uint16_t data;

	circbuff_pull(&st7920.data, &data);

	bytes[0] = data >> 8;
	bytes[1] = data & 0x00F0;
	bytes[2] = (data << 4) & 0x00F0;

	SPI_write(&st7920.spi, LCD_SPI_SLAVEID, bytes, 3, _LCD_spi_complete_transfer);
}

static void _LCD_push_buffer(enum LCD_RS aRS, uint8_t aData)
{
	uint16_t tmpData = (uint16_t)aData | ((uint16_t)aRS << 8);
	while(circbuff_isFull(&st7920.data));
	circbuff_push(&st7920.data, &tmpData);

	if(!st7920.isBusy) {
		_LCD_transfer();
	}
}