#include <p18f4550.h>
#include <types.h>
#define LCDINT_H_NO_EXTERN 1
#include "lcdint.h"


/* Status */
uint8_t lcd_status = 0;

/* Access control bits of LCD controller */
#define LCD_RS_INSTRUCTION     LATE &= 0b11111110;
#define LCD_RS_DATA            LATE |= 0b00000001;
#define LCD_RW_WRITE           LATE &= 0b11111101;
#define LCD_RW_READ            LATE |= 0b00000010;
#define LCD_E_LOW              LATE &= 0b11111011;
#define LCD_E_HIGH             LATE |= 0b00000100;

/* x should be constant here! */
#define TMR0_SET_DELAY(x)       TMR0H = (-(x * (int32_t)(TMR0FACTOR))) >> 8; TMR0L = (-(x * (int32_t)(TMR0FACTOR)));
#define TMR0_START             T0CON |= 0b10000000;
#define TMR0_STOP              T0CON &= 0b01111111;
#define TMR0_CLEAR_INT        INTCON &= 0b11111011;


/* Virtual LCD screen */
char lcd_ddram[LCD_WIDTH * LCD_HEIGHT];  /* Display RAM how it should be, magic value of 15 means updated */
uint8_t lcd_cgram[64];                   /* Chargen RAM how it should be, magic value of -1 means updated */
uint8_t lcd_x, lcd_y;                    /* User write position in DDRAM */

uint8_t lcd_address;                     /* Address in LCD.  Range: [0,127] if DDRAM, [128,191] for CGRAM (offset by 128) */
uint8_t lcd_cfg_display;                 /* Bit 0: blinking cursor; bit 1: line cursor; bit 2: display on */


/* Set dirty bit */
void lcd_update_cgram()
{
	uint8_t saved_INTCON;

	BEGIN_LOCK;
	if (lcd_status & LCD_STATUS_INITBIT)
	{
		if (lcd_status & LCD_STATUS_IDLEBIT)
		{
			lcd_status = LCD_STATUS_ALL_CLEAN & ~LCD_STATUS_CLEAN_CGRAM;
			TMR0H = 0xff; TMR0L = 0xff;  /* Call lcd_handle() immediately */
		}
		else
			lcd_status &= ~LCD_STATUS_CLEAN_CGRAM;
	}
	END_LOCK;
}
void lcd_update_ddram()
{
	uint8_t saved_INTCON;

	BEGIN_LOCK;
	if (lcd_status & LCD_STATUS_INITBIT)
	{
		if (lcd_status & LCD_STATUS_IDLEBIT)
		{
			lcd_status = LCD_STATUS_ALL_CLEAN & ~LCD_STATUS_CLEAN_DDRAM;
			TMR0H = 0xff; TMR0L = 0xff;  /* Call lcd_handle() immediately */
		}
		else
			lcd_status &= ~LCD_STATUS_CLEAN_DDRAM;
	}
	END_LOCK;
}
void lcd_config_display(uint8_t cfg)
{
	uint8_t saved_INTCON;

	lcd_cfg_display = cfg;

	BEGIN_LOCK;
	if (lcd_status & LCD_STATUS_INITBIT)
	{
		if (lcd_status & LCD_STATUS_IDLEBIT)
		{
			lcd_status = LCD_STATUS_ALL_CLEAN & ~LCD_STATUS_CLEAN_DISPLAY;
			TMR0H = 0xff; TMR0L = 0xff;  /* Call lcd_handle() immediately */
		}
		else
			lcd_status &= ~LCD_STATUS_CLEAN_DISPLAY;
	}
	END_LOCK;
}

void lcd_write(const char* s)
{
	char c;

	for(; c = *s; s++)
	{
		if ((c & 0b11111000) == 0b00001000)
		{
			/* Control character */
			if (c == 010)
			{
				if (lcd_x) lcd_x--;
			}
			else if (c == 011)
			{
				if (lcd_x < (uint8_t)LCD_WIDTH) lcd_x++;
			}
			else if (c == 012)
			{
				lcd_y ^= 1;
			}
			else if (c == 013)
			{
				lcd_x = 0; lcd_y = 0;
			}
			else if (c == 014)
			{
				lcd_x = 0; lcd_y = 0;
				for (c = 0; (uint8_t)c < sizeof(lcd_ddram); c++)
					lcd_ddram[(uint8_t)c] = ' ';
			}
			else if (c == 015)
			{
				lcd_x = 0;
			}
			else if (c == 017)
			{
				if (lcd_x >= (uint8_t)LCD_WIDTH) continue;
				lcd_ddram[lcd_x + lcd_y * LCD_WIDTH] = 0;
				lcd_x++;
			}
		}
		else
		{
			/* Ordinary character */
			if (lcd_x >= (uint8_t)LCD_WIDTH) continue;
			lcd_ddram[lcd_x + lcd_y * LCD_WIDTH] = c;
			lcd_x++;
		}
	}
	lcd_update_ddram();
}

void lcd_putc(char c)
{
	if (lcd_x >= (uint8_t)LCD_WIDTH) return;
	lcd_ddram[lcd_x + lcd_y * LCD_WIDTH] = c;
	lcd_x++;
	lcd_update_ddram();
}


void lcd_init()
{
	int8_t* it;
	uint8_t i;

	TRISC &= 0b11111110;
	TRISD = 0;
	TRISE &= 0b11111000;
	LCD_RW_WRITE;
	LCD_RS_DATA;
	LCD_E_LOW;
	lcd_backlight_off();  /* Backlight off per default */

	/* Set Timer0 prescaler, set 16-bit mode, disable Timer0 */
#if (TMR0PRESCALE == 32)
	T0CON = 0b00010100;
#elif (TMR0PRESCALE == 16)
	T0CON = 0b00010011;
#elif (TMR0PRESCALE == 8)
	T0CON = 0b00010010;
#elif (TMR0PRESCALE == 4)
	T0CON = 0b00010001;
#elif (TMR0PRESCALE == 2)
	T0CON = 0b00010000;
#elif (TMR0PRESCALE == 1)
	T0CON = 0b00011000;
#else
#error "TMR0PRESCALE must be 1, 2, 4, 8, 16 or 32"
#endif

	/* Initialize data */
	for (i = 0; i < sizeof(lcd_cgram); i++) lcd_cgram[i] = cgram_clean;
	for (i = 0; i < sizeof(lcd_ddram); i++) lcd_ddram[i] = 32;

	lcd_address = 127;  /* Invalid */
	lcd_x = 0; lcd_y = 0;
	lcd_cfg_display = 0b00000100;  /* Display on */

	lcd_status = LCD_STATUS_INIT0;

	/* Power-up delay of LCD */
	TMR0_SET_DELAY(4050); /* 15ms */
	TMR0_CLEAR_INT;
	TMR0_START;
}


void lcd_handle()
{
	uint8_t saved_INTCON;
	uint8_t command;             /* Which command to send? */
	uint8_t command_is_data = 0; /* 1 if data, 0 if instruction */
	uint8_t prevD;
	int8_t x;
	uint8_t a;

	TMR0_STOP;
	TMR0_CLEAR_INT;

idle_check:
	if (lcd_status >= 0b10111111u)
	{
		/* IDLE */
		if (lcd_status != LCD_STATUS_IDLE_MAX)
		{
			TMR0_SET_DELAY(12500); /* 50ms without 8% extra */
			/* HACK: if lcd_status was 0b10111111, this increment will
			 * "overflow" and set the IDLE bit */
			lcd_status++;
		}
		/* If lcd_status already was LCD_STATUS_IDLE_MAX,
		 * we just let the timer run. */
		TMR0_START;
		return;
	}

	if ((lcd_status & LCD_STATUS_INITBIT) == 0)
	{
		/* INITIALIZATION SEQUENCE */
		command = 0b00110000;
		if (lcd_status == LCD_STATUS_INIT0)
		{
			lcd_status = LCD_STATUS_INIT1;
			TMR0_SET_DELAY(1107); /* 4.1ms */
		}
		else if (lcd_status == LCD_STATUS_INIT1)
		{
			lcd_status = LCD_STATUS_INIT2;
			TMR0_SET_DELAY(27); /* 100us */
		}
		else if (lcd_status == LCD_STATUS_INIT2)
		{
			lcd_status = LCD_STATUS_INITBIT | LCD_STATUS_CLEAN_SHIFT;
			TMR0_SET_DELAY(27); /* 100us */
		}
	}
	else /* NORMAL OPERATION */
	{
		/* Test configuation commands */
		if (~lcd_status & (LCD_STATUS_CLEAN_FUNC|LCD_STATUS_CLEAN_DISPLAY|LCD_STATUS_CLEAN_ENTRY_MODE))
		{
			if ((lcd_status & LCD_STATUS_CLEAN_FUNC) == 0)
			{
#if (LCD_HEIGHT == 1)
				command = 0b00110000;
#else
				command = 0b00111000;
#endif
				lcd_status |= LCD_STATUS_CLEAN_FUNC;
				TMR0_SET_DELAY(10); /* 37us */
			}
			else if ((lcd_status & LCD_STATUS_CLEAN_DISPLAY) == 0)
			{
				command = 0b00001000 + lcd_cfg_display;
				lcd_status |= LCD_STATUS_CLEAN_DISPLAY;
				TMR0_SET_DELAY(10); /* 37us */
			}
			else /* if ((lcd_status & LCD_STATUS_CLEAN_ENTRY_MODE) == 0) */
			{
				command = 0b00000110;
				lcd_status |= LCD_STATUS_CLEAN_ENTRY_MODE;
				TMR0_SET_DELAY(10); /* 37us */
			}
		}
		else if ((lcd_status & LCD_STATUS_CLEAN_CGRAM) == 0)
		{
			/* Check whether we should send a cgram byte now */
			if (
				(lcd_address & 128u)     /* Are we in CGRAM? */
				&& (lcd_address < 192u)  /* Have we not overflowed? */
				&& (command = lcd_cgram[a = (lcd_address - 128u)]) != cgram_clean  /* Is the byte dirty? */
			)
			{
				/* Send byte */
				command_is_data = 1;
				lcd_cgram[a] = cgram_clean;
				lcd_address++;
				TMR0_SET_DELAY(12); /* 44us */
			}
			else
			{
				/* Clean: change address to first dirty char found */
				for (a = 0; a < sizeof(lcd_cgram); a++)
				{
					if (lcd_cgram[a] != cgram_clean) {lcd_address = 128u + a; goto found_dirty_cgram;}
				}
				/* All CGRAM is clean: try again to find something else to do */
				lcd_status |= LCD_STATUS_CLEAN_CGRAM;
				goto idle_check;
found_dirty_cgram:
				/* Set address */
				command = 0b11000000 + lcd_address;   /* Note the term 0b10000000 in lcd_address */
				TMR0_SET_DELAY(10); /* 37us */
			}
		}
		else /* if ((lcd_status & LCD_STATUS_CLEAN_CGRAM) == 0) */
		{
			/* Check whether we should send a data byte now */
			if (
				!(lcd_address & 128u)  /* Are we in DDRAM? */
				&& (x = lcd_address & 0x3f) < LCD_WIDTH  /* Are we on the visible screen? */
				&& (command = lcd_ddram[a = x + (lcd_address >> 6) * LCD_WIDTH]) != ddram_clean  /* Is the byte dirty? */
			)
			{
				/* Send byte */
				command_is_data = 1;
				lcd_ddram[a] = ddram_clean;
				lcd_address++;
				TMR0_SET_DELAY(12); /* 44us */
			}
			else
			{
				/* Clean: change address to leftmost dirty char found */
				for (x = 0; x < LCD_WIDTH; x++)
				{
					if (lcd_ddram[x] != ddram_clean) {lcd_address = x; goto found_dirty_ddram;}
#if (LCD_HEIGHT > 1)
					if ((lcd_ddram+LCD_WIDTH)[x] != ddram_clean) {lcd_address = x + 0x40; goto found_dirty_ddram;}
#endif
				}
				/* All DDRAM is clean */
				lcd_status |= LCD_STATUS_CLEAN_DDRAM;
				/* Position the cursor correctly */
				a = lcd_x + lcd_y * 0x40;  /* Address where the cursor is supposed to be */
				if (lcd_address == a)
				{
					/* Cursor is okay: we are idle! */
					goto idle_check;
				}
				lcd_address = a;
found_dirty_ddram:
				/* Set address */
				command = 0b10000000 | lcd_address;
				TMR0_SET_DELAY(10); /* 37us */
			}
		}
	}

	/* Actually send the command */
	BEGIN_LOCK;  /* Disable interrupts so this code is really executed sequentially */
	LCD_RW_WRITE;
	LCD_RS_DATA;
	if (!command_is_data) LCD_RS_INSTRUCTION;
	LCD_E_HIGH;
	prevD = LATD;
	LATD = command;
	TMR0_START;  /* Actual start is 2 Tcy later */
	LCD_E_LOW;
	LATD = prevD;
	END_LOCK;  /* Looking at the assembly code, we were locked for 23 Tcy */

	/* Put nothing here after END_LOCK! */
}

