/* Reflow Oven Controller
*
* Copyright (C) 2020  Mario Hüttel <mario.huettel@gmx.net>
*
* This file is part of the Reflow Oven Controller Project.
*
* The reflow oven controller is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* The Reflow Oven Control Firmware is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with the reflow oven controller project.
* If not, see <http://www.gnu.org/licenses/>.
*/

/* Thanks to
 * https://www.mikrocontroller.net/articles/AVR-GCC-Tutorial/LCD-Ansteuerung
 * for the basic code construct
 */

#include <stm32/stm32f4xx.h>
#include <reflow-controller/ui/lcd.h>
#include <reflow-controller/systick.h>
#include <stm-periph/clock-enable-manager.h>
#include <stm-periph/stm32-gpio-macros.h>
#include <stdbool.h>
#include <string.h>

static void lcd_port_clear(void)
{
	LCD_DPORT->ODR &= ~(LCD_E_MASK);
	LCD_DPORT->ODR &= ~(LCD_DATA_MASK | LCD_RS_MASK | LCD_E_MASK);
}

static void lcd_enable(void)
{
	LCD_DPORT->ODR |= LCD_E_MASK;
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
	//systick_wait_ms(10);
	LCD_DPORT->ODR &= ~LCD_E_MASK;
	//systick_wait_ms(10);
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
}

static void lcd_out(uint8_t data)
{
	LCD_DPORT->ODR &= ~(LCD_DATA_MASK);
	LCD_DPORT->ODR |= (data << LCD_DATA_BIT_OFFSET) & LCD_DATA_MASK;
}

static void lcd_data(uint8_t data)
{
	lcd_port_clear();
	LCD_DPORT->ODR |= LCD_RS_MASK;
	lcd_out((data>>4) & 0xFU);
	lcd_enable();
	lcd_out(data & 0xFU);
	lcd_enable();
	systick_wait_ms(1);
}

static void lcd_command(uint8_t data)
{
	lcd_port_clear();
	lcd_out((data>>4) & 0xFU);
	lcd_enable();
	lcd_out(data & 0xFU);
	lcd_enable();
	systick_wait_ms(1);
}

#define LCD_DDADR_LINE1         0x00
#define LCD_DDADR_LINE2         0x40
#define LCD_DDADR_LINE3         0x10
#define LCD_DDADR_LINE4         0x50

// Clear Display -------------- 0b00000001
#define LCD_CLEAR_DISPLAY       0x01

// Cursor Home ---------------- 0b0000001x
#define LCD_CURSOR_HOME         0x02

// Set Entry Mode ------------- 0b000001xx
#define LCD_SET_ENTRY           0x04

#define LCD_ENTRY_DECREASE      0x00
#define LCD_ENTRY_INCREASE      0x02
#define LCD_ENTRY_NOSHIFT       0x00
#define LCD_ENTRY_SHIFT         0x01

// Set Display ---------------- 0b00001xxx
#define LCD_SET_DISPLAY         0x08

#define LCD_DISPLAY_OFF         0x00
#define LCD_DISPLAY_ON          0x04
#define LCD_CURSOR_OFF          0x00
#define LCD_CURSOR_ON           0x02
#define LCD_BLINKING_OFF        0x00
#define LCD_BLINKING_ON         0x01

// Set Shift ------------------ 0b0001xxxx
#define LCD_SET_SHIFT           0x10

#define LCD_CURSOR_MOVE         0x00
#define LCD_DISPLAY_SHIFT       0x08
#define LCD_SHIFT_LEFT          0x00
#define LCD_SHIFT_RIGHT         0x04

// Set Function --------------- 0b001xxxxx
#define LCD_SET_FUNCTION        0x20

#define LCD_FUNCTION_4BIT       0x00
#define LCD_FUNCTION_8BIT       0x10
#define LCD_FUNCTION_1LINE      0x00
#define LCD_FUNCTION_2LINE      0x08
#define LCD_FUNCTION_5X7        0x00
#define LCD_FUNCTION_5X10       0x04

#define LCD_SOFT_RESET          0x30

// Set CG RAM Address --------- 0b01xxxxxx  (Character Generator RAM)
#define LCD_SET_CGADR           0x40

#define LCD_GC_CHAR0            0
#define LCD_GC_CHAR1            1
#define LCD_GC_CHAR2            2
#define LCD_GC_CHAR3            3
#define LCD_GC_CHAR4            4
#define LCD_GC_CHAR5            5
#define LCD_GC_CHAR6            6
#define LCD_GC_CHAR7            7

// Set DD RAM Address --------- 0b1xxxxxxx  (Display Data RAM)
#define LCD_SET_DDADR           0x80

static char shadow_display[4][21];

void lcd_clear(void)
{
	lcd_command(LCD_CLEAR_DISPLAY);
	systick_wait_ms(3);
}

void lcd_home(void)
{
	lcd_command(LCD_CURSOR_HOME);
	systick_wait_ms(3);
}

static uint8_t lcd_get_set_cursor_cmd(uint8_t x, uint8_t y)
{
	uint8_t data;

	switch (y) {
	case 0:
		/* First line */
		data = LCD_SET_DDADR + LCD_DDADR_LINE1 + x;
		break;

	case 1:
		/* Second Line */
		data = LCD_SET_DDADR + LCD_DDADR_LINE2 + x;
		break;

	case 2:
		/* Third line */
		data = LCD_SET_DDADR + LCD_DDADR_LINE3 + x;
		break;

	case 3:
		/* Fourth line */
		data = LCD_SET_DDADR + LCD_DDADR_LINE4 + x;
		break;

	default:
		/* In case of wrong line, assume first line */
		data = LCD_SET_DDADR + LCD_DDADR_LINE1;
		break;
	}

	return data;
}

void lcd_setcursor(uint8_t x, uint8_t y)
{
	uint8_t data;

	data = lcd_get_set_cursor_cmd(x, y);
	lcd_command(data);
}

void lcd_string(const char *data)
{
	while (*data != '\0')
		lcd_data((uint8_t)*data++);
}

static void lcd_port_init()
{
	LCD_DPORT->MODER &= MODER_DELETE(LCD_E) & MODER_DELETE(LCD_RS) & MODER_DELETE(LCD_DATA_BIT_OFFSET) &
			MODER_DELETE(LCD_DATA_BIT_OFFSET + 1) & MODER_DELETE(LCD_DATA_BIT_OFFSET + 2) &
			MODER_DELETE(LCD_DATA_BIT_OFFSET + 3);

	LCD_DPORT->MODER |= OUTPUT(LCD_E) | OUTPUT(LCD_RS) | OUTPUT(LCD_DATA_BIT_OFFSET) |
			OUTPUT(LCD_DATA_BIT_OFFSET + 1) | OUTPUT(LCD_DATA_BIT_OFFSET + 2) |
			OUTPUT(LCD_DATA_BIT_OFFSET + 3);
}

static void lcd_clear_shadow_buff()
{
	int i, j;

	for (i = 0; i < 4; i++) {
		for (j = 0; j < 21; j++) {
			shadow_display[i][j] = 0x0;
		}
	}
}

void lcd_init(void)
{
	int i;

	rcc_manager_enable_clock(&RCC->AHB1ENR, BITMASK_TO_BITNO(LCD_RCC_MASK));
	lcd_port_init();
	lcd_port_clear();

	lcd_clear_shadow_buff();

	systick_wait_ms(100);
	LCD_DPORT->ODR |= (0x3 << LCD_DATA_BIT_OFFSET);

	for (i = 0; i < 3; i++) {
		lcd_enable();
		systick_wait_ms(5);
	}

	// Set 4 Bit mode
	lcd_port_clear();
	LCD_DPORT->ODR |= (0x2<<LCD_DATA_BIT_OFFSET);
	lcd_enable();
	systick_wait_ms(2);

	/* 4 Bit mode 2 lines */
	lcd_command( LCD_SET_FUNCTION |
		     LCD_FUNCTION_4BIT |
		     LCD_FUNCTION_2LINE |
		     LCD_FUNCTION_5X7 );

	/* Display on without cursor */
	lcd_command( LCD_SET_DISPLAY |
		     LCD_DISPLAY_ON |
		     LCD_CURSOR_OFF |
		     LCD_BLINKING_OFF);

	/* Cursor increment, no scroll */
	lcd_command( LCD_SET_ENTRY |
		     LCD_ENTRY_INCREASE |
		     LCD_ENTRY_NOSHIFT );

	lcd_clear();
}

void lcd_deinit()
{
	lcd_port_clear();
	rcc_manager_disable_clock(&RCC->AHB1ENR, BITMASK_TO_BITNO(LCD_RCC_MASK));
}

static uint8_t compare_input_to_shadow(const char (*display_buffer)[21])
{
	uint8_t ret = 0;
	int i, row;

	for (row = 0; row < 4; row++) {
		for (i = 0; i < 20; i++) {
			if (display_buffer[row][i] != shadow_display[row][i]) {
				ret |= (1U<<row);
				break;
			}
		}
	}

	return ret;
}

static void lcd_fsm_enable(bool en)
{
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
	if (en)
		LCD_DPORT->ODR |= LCD_E_MASK;
	else
		LCD_DPORT->ODR &= ~LCD_E_MASK;
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
	__ASM("nop");
}

static void lcd_fsm_write_command(bool high, uint8_t cmd)
{
	lcd_port_clear();

	if (high)
		lcd_out((cmd >> 4) & 0x0F);
	else
		lcd_out((cmd) & 0x0F);
}

static void lcd_fsm_write_data(bool high, uint8_t data)
{
	LCD_DPORT->ODR |= (1<<LCD_RS);
	if (high)
		lcd_out((data >> 4) & 0x0F);
	else
		lcd_out((data) & 0x0F);
}

enum lcd_fsm_ret lcd_fsm_write_buffer(const char (*display_buffer)[21])
{
	static bool idle = true;
	static uint8_t rows_to_handle = 0;
	static uint32_t state_cnt;
	static uint8_t row_cnt = 0;
	static uint32_t char_cnt;
	static uint32_t line_len;
	static uint64_t timestamp = 0ULL;
	enum lcd_fsm_ret ret;

	ret = LCD_FSM_NOP;

	if (idle) {
		rows_to_handle = compare_input_to_shadow(display_buffer);
		memcpy(shadow_display, display_buffer, sizeof(shadow_display));
		shadow_display[0][20] = 0;
		shadow_display[1][20] = 0;
		shadow_display[2][20] = 0;
		shadow_display[3][20] = 0;
		state_cnt = 0;
		row_cnt = 0;
		idle = false;
	}

	if (rows_to_handle == 0) {
		idle = true;
		return ret;
	}

	if ((rows_to_handle & (1<<row_cnt))) {
		switch (state_cnt) {
		case 0:
			lcd_fsm_write_command(true, lcd_get_set_cursor_cmd(0, row_cnt));
			line_len = strlen(&shadow_display[row_cnt][0]);
			char_cnt = 0;
			lcd_fsm_enable(true);
			ret = LCD_FSM_WAIT_CALL;
			state_cnt++;
			break;
		case 1:
			lcd_fsm_enable(false);
			ret = LCD_FSM_WAIT_CALL;
			state_cnt++;
			break;
		case 2:
			lcd_fsm_write_command(false, lcd_get_set_cursor_cmd(0, row_cnt));
			lcd_fsm_enable(true);
			ret = LCD_FSM_WAIT_CALL;
			state_cnt++;
			break;
		case 3:
			lcd_fsm_enable(false);
			ret = LCD_FSM_WAIT_CALL;
			timestamp = systick_get_global_tick();
			state_cnt++;
			break;
		case 4:
			if (!systick_ticks_have_passed(timestamp, 5)) {
				ret = LCD_FSM_WAIT_CALL;
			} else {
				ret = LCD_FSM_CALL_AGAIN;
				state_cnt++;
			}
			break;
		case 5:
			lcd_fsm_write_data(true, (char_cnt >= line_len) ? ' ' : shadow_display[row_cnt][char_cnt]);
			lcd_fsm_enable(true);
			ret = LCD_FSM_CALL_AGAIN;
			state_cnt++;
			break;
		case 6:
			lcd_fsm_enable(false);
			ret = LCD_FSM_WAIT_CALL;
			state_cnt++;
			break;
		case 7:
			lcd_fsm_write_data(false, (char_cnt >= line_len) ? ' ' : shadow_display[row_cnt][char_cnt]);
			lcd_fsm_enable(true);
			ret = LCD_FSM_CALL_AGAIN;
			state_cnt++;
			break;
		case 8:
			lcd_fsm_enable(false);
			ret = LCD_FSM_WAIT_CALL;
			char_cnt++;
			if (char_cnt < LCD_CHAR_WIDTH) {
				state_cnt = 5;
			} else {
				state_cnt = 0;
				rows_to_handle &= (uint8_t)~(1U<<row_cnt);
				if (row_cnt < 3) {
					row_cnt++;
				} else {
					idle = true;
					row_cnt = 0;
				}
			}
			break;
		default:
			ret = LCD_FSM_NOP;
			idle = true;
			rows_to_handle = 0U;
			break;
		}
	} else {
		if (row_cnt < 3) {
			row_cnt++;
			ret = LCD_FSM_CALL_AGAIN;
		} else {
			row_cnt = 0;
			ret = LCD_FSM_NOP;
		}
	}

	return ret;
}
