/*
    ChibiOS/RT - Copyright (C) 2012
                 Joel Bodenmann aka Tectu <joel@unormal.org>

    This file is part of ChibiOS-LCD-Driver.

    ChibiOS-LCD-Driver is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 3 of the License, or
    (at your option) any later version.

    ChibiOS-LCD-Driver 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 this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "ssd1289_lld.h"

#ifdef LCD_USE_SSD1289

uint8_t orientation;
uint16_t DeviceCode;
extern uint16_t lcd_width, lcd_height;

static uint16_t buf[((SCREEN_HEIGHT > SCREEN_WIDTH ) ? SCREEN_HEIGHT : SCREEN_WIDTH)];

#ifdef LCD_USE_GPIO

static __inline void lld_lcdWriteIndex(uint16_t index) {
    Clr_RS;
    Set_RD;
  
	lld_lcdWriteGPIO(index);

    Clr_WR;
    Set_WR;
}

static __inline void lld_lcdWriteData(uint16_t data) {
	Set_RS;

	lld_lcdWriteGPIO(data);

	Clr_WR;
	Set_WR;
}

static __inline void lld_lcdWriteReg(uint16_t lcdReg,uint16_t lcdRegValue) { 
    Clr_CS;

    lld_lcdWriteIndex(lcdReg);    
    lld_lcdWriteData(lcdRegValue);  

    Set_CS; 
}

static __inline uint16_t lld_lcdReadData(void) {
	uint16_t value;

	Set_RS;
	Set_WR;
	Clr_RD;

	value = lld_lcdReadGPIO();

    Set_RD;

	return value;
}

static __inline uint16_t lld_lcdReadReg(uint16_t lcdReg) {
    uint16_t lcdRAM;

    Clr_CS;
    lld_lcdWriteIndex(lcdReg);
    lcdRAM = lld_lcdReadData();
                 
    Set_CS;

    return lcdRAM;
}

__inline void lld_lcdWriteStreamStart(void) {
	Clr_CS;

	lld_lcdWriteIndex(0x0022);
}

__inline void lld_lcdWriteStreamStop(void) {
	Set_CS;
}

__inline void lld_lcdWriteStream(uint16_t *buffer, uint16_t size) {
	uint16_t i;

	Set_RS;

	for(i = 0; i < size; i++) {
		lld_lcdWriteGPIO(buffer[i]);
		Clr_WR;
		Set_WR;
	}
}

__inline void lld_lcdReadStreamStart(void) {
	Clr_CS

	lld_lcdWriteIndex(0x0022);
}

__inline void lld_lcdReadStreamStop(void) {
	Set_CS;
}

__inline void lld_lcdReadStream(uint16_t *buffer, size_t size) {
	uint16_t i;
	volatile uint16_t dummy;

	dummy = lld_lcdReadData();
	for(i = 0; i < size; i++)
		buffer[i] = lld_lcdReadData();
}

#endif // LCD_USE_GPIO 

#ifdef LCD_USE_SPI
	/* TODO */
#endif // LCD_USE_SPI

#ifdef LCD_USE_FSMC

#define LCD_REG              (*((volatile uint16_t *) 0x60000000)) /* RS = 0 */
#define LCD_RAM              (*((volatile uint16_t *) 0x60020000)) /* RS = 1 */

static __inline void lld_lcdWriteIndex(uint16_t index) {
	LCD_REG = index;
}

static __inline void lld_lcdWriteData(uint16_t data) {
	LCD_RAM = data;
}

static __inline void lld_lcdWriteReg(uint16_t lcdReg,uint16_t lcdRegValue) {
	LCD_REG = lcdReg;
	LCD_RAM = lcdRegValue;
}

static __inline uint16_t lld_lcdReadData(void) {
	return (LCD_RAM);
}

static __inline uint16_t lld_lcdReadReg(uint16_t lcdReg) {
	LCD_REG = lcdReg;
	volatile uint16_t dummy = LCD_RAM;
	return (LCD_RAM);
}

__inline void lld_lcdWriteStreamStart(void) {
	LCD_REG = 0x0022;
}

__inline void lld_lcdWriteStreamStop(void) {

}

__inline void lld_lcdWriteStream(uint16_t *buffer, uint16_t size) {
	uint16_t i;
	for(i = 0; i < size; i++)
		LCD_RAM = buffer[i];
}

__inline void lld_lcdReadStreamStart(void) {
	LCD_REG = 0x0022;
}

__inline void lld_lcdReadStreamStop(void) {

}

__inline void lld_lcdReadStream(uint16_t *buffer, size_t size) {
	uint16_t i;
	/* throw away first value read */
	volatile uint16_t dummy = LCD_RAM;

	for(i = 0; i < size; i++) {
		buffer[i] = LCD_RAM;
	}
}
#endif // LCD_USE_FSMC

static __inline void lld_lcdDelay(uint16_t us) {
	chThdSleepMicroseconds(us);
}

void lld_lcdSetPowerMode(uint8_t powerMode) {
	switch(powerMode) {
		case powerOff:
			lld_lcdWriteReg(0x0010, 0x0000);	// leave sleep mode
			lld_lcdWriteReg(0x0007, 0x0000);	// halt operation
			lld_lcdWriteReg(0x0000, 0x0000);	// turn off oszillator
			lld_lcdWriteReg(0x0010, 0x0001);	// enter sleepmode
			break;
		case powerOn:
			lld_lcdWriteReg(0x0010, 0x0000);	// leave sleep mode
			lld_lcdInit();
			break;
		case sleepOn:
			lld_lcdWriteReg(0x0010, 0x0001);	// enter sleep mode
			break;
		case sleepOff:
			lld_lcdWriteReg(0x0010, 0x0000);	// leave sleep mode
			break;
	}
}

void lld_lcdSetCursor(uint16_t x, uint16_t y) {
	/* Reg 0x004E is an 8 bit value
	 * Reg 0x004F is 9 bit
	 * Use a bit mask to make sure they are not set too high
	 */
	switch(lcdGetOrientation()) {
		case portraitInv:
			lld_lcdWriteReg(0x004e, (SCREEN_WIDTH-1-x) & 0x00FF);
			lld_lcdWriteReg(0x004f, (SCREEN_HEIGHT-1-y) & 0x01FF);
			break;
		case portrait:
			lld_lcdWriteReg(0x004e, x & 0x00FF);
			lld_lcdWriteReg(0x004f, y & 0x01FF);
			break;
		case landscape:
			lld_lcdWriteReg(0x004e, y & 0x00FF);
			lld_lcdWriteReg(0x004f, x & 0x01FF);
			break;
		case landscapeInv:
			lld_lcdWriteReg(0x004e, (SCREEN_WIDTH - y - 1) & 0x00FF);
			lld_lcdWriteReg(0x004f, (SCREEN_HEIGHT - x - 1) & 0x01FF);
			break;
	}
}

void lld_lcdSetOrientation(uint8_t newOrientation) {
    orientation = newOrientation;

    switch(orientation) {
        case portrait:
            lld_lcdWriteReg(0x0001, 0x2B3F);
            /* ID = 11 AM = 0 */
            lld_lcdWriteReg(0x0011, 0x6070);
            lcd_height = SCREEN_HEIGHT;
            lcd_width = SCREEN_WIDTH;
            break;
        case landscape:
            lld_lcdWriteReg(0x0001, 0x293F);
            /* ID = 11 AM = 1 */
            lld_lcdWriteReg(0x0011, 0x6078);
            lcd_height = SCREEN_WIDTH;
            lcd_width = SCREEN_HEIGHT;
            break;
        case portraitInv:
            lld_lcdWriteReg(0x0001, 0x2B3F);
            /* ID = 01 AM = 0 */
            lld_lcdWriteReg(0x0011, 0x6040);
            lcd_height = SCREEN_HEIGHT;
            lcd_width = SCREEN_WIDTH;
            break;
        case landscapeInv:
            lld_lcdWriteReg(0x0001, 0x293F);
            /* ID = 01 AM = 1 */
            lld_lcdWriteReg(0x0011, 0x6048);
            lcd_height = SCREEN_WIDTH;
            lcd_width = SCREEN_HEIGHT;
            break;
    }
}

void lld_lcdSetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
    lld_lcdSetCursor(x0, y0);

    /* Reg 0x44 - Horizontal RAM address position
     * 		Upper Byte - HEA
     * 		Lower Byte - HSA
     * 		0 <= HSA <= HEA <= 0xEF
     * Reg 0x45,0x46 - Vertical RAM address position
     * 		Lower 9 bits gives 0-511 range in each value
     * 		0 <= Reg(0x45) <= Reg(0x46) <= 0x13F
     */

    switch(lcdGetOrientation()) {
        case portrait:
            lld_lcdWriteReg(0x44, (((x1-1) << 8) & 0xFF00 ) | (x0 & 0x00FF));
            lld_lcdWriteReg(0x45, y0 & 0x01FF);
            lld_lcdWriteReg(0x46, (y1-1) & 0x01FF);
            break;
        case landscape:
            lld_lcdWriteReg(0x44, (((y1-1) << 8) & 0xFF00) | (y1 & 0x00FF));
            lld_lcdWriteReg(0x45, x0 & 0x01FF);
            lld_lcdWriteReg(0x46, (x1-1) & 0x01FF);
            break;
        case portraitInv:
            lld_lcdWriteReg(0x44, (((SCREEN_WIDTH-x0-1) & 0x00FF) << 8) | ((SCREEN_WIDTH - x1) & 0x00FF));
            lld_lcdWriteReg(0x45, (SCREEN_HEIGHT-y1) & 0x01FF);
            lld_lcdWriteReg(0x46, (SCREEN_HEIGHT-y0-1) & 0x01FF);
            break;
        case landscapeInv:
            lld_lcdWriteReg(0x44, (((SCREEN_WIDTH - y0 - 1) & 0x00FF) << 8) | ((SCREEN_WIDTH - y1) & 0x00FF));
            lld_lcdWriteReg(0x45, (SCREEN_HEIGHT - x1) & 0x01FF);
            lld_lcdWriteReg(0x46, (SCREEN_HEIGHT - x0 - 1) & 0x01FF);
            break;
    }

    lld_lcdSetCursor(x0, y0);
}

void lld_lcdFillArea(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) {
	uint32_t index = 0, area;

	area = ((x1-x0)*(y1-y0));

    lld_lcdSetWindow(x0, y0, x1, y1);
    lld_lcdWriteStreamStart();

    for(index = 0; index < area; index++)
        lld_lcdWriteData(color);

    lld_lcdWriteStreamStop();
}

void lld_lcdClear(uint16_t color) {
    uint32_t index = 0;

    lld_lcdSetCursor(0, 0);
    lld_lcdWriteStreamStart();

    for(index = 0; index < SCREEN_WIDTH * SCREEN_HEIGHT; index++)
    	lld_lcdWriteData(color);

    lld_lcdWriteStreamStop();
}

uint16_t lld_lcdGetPixelColor(uint16_t x, uint16_t y) {
    uint16_t dummy;

    lld_lcdSetCursor(x,y);
    lld_lcdWriteStreamStart();

    dummy = lld_lcdReadData();
    dummy = lld_lcdReadData();

    lld_lcdWriteStreamStop();

	return dummy;
}

void lld_lcdDrawPixel(uint16_t x, uint16_t y, uint16_t color) {
    lld_lcdSetCursor(x, y);
    lld_lcdWriteReg(0x0022, color);
}

void lld_lcdInit(void) {
#ifdef LCD_USE_FSMC
	/* FSMC setup. TODO: this only works for STM32F1 */
	rccEnableAHB(RCC_AHBENR_FSMCEN, 0);
	int FSMC_Bank = 0;
	/* timing structure */
	/* from datasheet:
        address setup: 0ns
        address hold: 0ns
        Data setup: 5ns
        Data hold: 5ns
        Data access: 250ns
        output hold: 100ns
	 */
	FSMC_Bank1->BTCR[FSMC_Bank+1] = FSMC_BTR1_ADDSET_1 | FSMC_BTR1_DATAST_1;

	/* Bank1 NOR/SRAM control register configuration */
	FSMC_Bank1->BTCR[FSMC_Bank] = FSMC_BCR1_MWID_0 | FSMC_BCR1_WREN | FSMC_BCR1_MBKEN;
#endif
	DeviceCode = lld_lcdReadReg(0x0000);

	lld_lcdWriteReg(0x0000,0x0001);		lld_lcdDelay(5);
    lld_lcdWriteReg(0x0003,0xA8A4);    	lld_lcdDelay(5);   
    lld_lcdWriteReg(0x000C,0x0000);    	lld_lcdDelay(5);   
    lld_lcdWriteReg(0x000D,0x080C);    	lld_lcdDelay(5);   
    lld_lcdWriteReg(0x000E,0x2B00);    	lld_lcdDelay(5);   
    lld_lcdWriteReg(0x001E,0x00B0);    	lld_lcdDelay(5);  
	lld_lcdWriteReg(0x0001,0x2B3F);		lld_lcdDelay(5);
    lld_lcdWriteReg(0x0002,0x0600);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0010,0x0000);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0011,0x6070);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0005,0x0000);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0006,0x0000);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0016,0xEF1C);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0017,0x0003);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0007,0x0133);    	lld_lcdDelay(5);         
    lld_lcdWriteReg(0x000B,0x0000);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x000F,0x0000);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0041,0x0000);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0042,0x0000);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0048,0x0000);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0049,0x013F);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x004A,0x0000);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x004B,0x0000);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0044,0xEF00);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0045,0x0000);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0046,0x013F);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0030,0x0707);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0031,0x0204);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0032,0x0204);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0033,0x0502);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0034,0x0507);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0035,0x0204);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0036,0x0204);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0037,0x0502);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x003A,0x0302);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x003B,0x0302);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0023,0x0000);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0024,0x0000);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x0025,0x8000);    	lld_lcdDelay(5);
    lld_lcdWriteReg(0x004f,0x0000);		lld_lcdDelay(5);      
    lld_lcdWriteReg(0x004e,0x0000);		lld_lcdDelay(5);
}

uint16_t lld_lcdGetOrientation(void) {
	return orientation;
}

uint16_t lld_lcdGetHeight(void) {
	return lcd_height;
}

uint16_t lld_lcdGetWidth(void) {
	return lcd_width;
}

/* a positive lines value shifts the screen up, negative down */
void lld_lcdVerticalScroll(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, int16_t lines) {
	uint16_t row0, row1;
	uint16_t i;

	lld_lcdSetWindow(x0, y0, x1, y1);

	for(i = 0; i < ((y1-y0) - abs(lines)); i++) {
		if(lines > 0) {
			row0 = y0 + i + lines;
			row1 = y0 + i;
		} else {
			row0 = (y1 - i - 1) + lines;
			row1 = (y1 - i - 1);
		}

		/* read row0 into the buffer and then write at row1*/
		lld_lcdSetWindow(x0, row0, x1, row0);
		lld_lcdReadStreamStart();
		lld_lcdReadStream(buf, x1-x0);
		lld_lcdReadStreamStop();

		lld_lcdSetWindow(x0, row1, x1, row1);
		lld_lcdWriteStreamStart();
		lld_lcdWriteStream(buf, x1-x0);
		lld_lcdWriteStreamStop();
	}
}

#endif