/*
    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 "ch.h"
#include "hal.h"

#include "s6d1121_lld.h"
#include "chprintf.h"

#ifdef LCD_USE_S6D1121

static uint8_t orientation;
extern uint16_t lcd_width, lcd_height;

/* all interfaces use RST via GPIO */
/* TODO: option to disable RST; assumes RST is tied high */
#define LCD_RST_LOW		palClearPad(LCD_RST_GPIO, LCD_RST_PIN)
#define LCD_RST_HIGH	palSetPad(LCD_RST_GPIO, LCD_RST_PIN)

#define s6d1121_delay(n) 	halPolledDelay(MS2RTT(n));

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

#if defined(LCD_USE_GPIO)

#define LCD_CS_LOW		palClearPad(LCD_CS_GPIO, LCD_CS_PIN)
#define LCD_CS_HIGH		palSetPad(LCD_CS_GPIO, LCD_CS_PIN)

#define LCD_RS_LOW		palClearPad(LCD_RS_GPIO, LCD_RS_PIN)
#define LCD_RS_HIGH		palSetPad(LCD_RS_GPIO, LCD_RS_PIN)

#define LCD_RD_LOW		palClearPad(LCD_RD_GPIO, LCD_RD_PIN)
#define LCD_RD_HIGH		palSetPad(LCD_RD_GPIO, LCD_RD_PIN)

#define LCD_WR_LOW		palClearPad(LCD_WR_GPIO, LCD_WR_PIN)
#define LCD_WR_HIGH		palSetPad(LCD_WR_GPIO, LCD_WR_PIN)

#define LCD_BL_LOW		palClearPad(LCD_BL_GPIO, LCD_BL_PIN)
#define LCD_BL_HIGH		palSetPad(LCD_BL_GPIO, LCD_BL_PIN)


static inline void lld_lcddelay(void)
{
	asm volatile ("nop");
	asm volatile ("nop");
}

static inline void lld_lcdwrite(uint16_t db)
{
	LCD_D4_GPIO->BSRR.W=((~db&0xFFF0)<<16)|(db&0xFFF0);
	LCD_D0_GPIO->BSRR.W=((~db&0x000F)<<16)|(db&0x000F);

	LCD_WR_LOW;
	lld_lcddelay();
	LCD_WR_HIGH;
}

static __inline uint16_t lld_lcdReadData(void) {
	uint16_t value=0;

	LCD_RS_HIGH;
	LCD_WR_HIGH;
	LCD_RD_LOW;

#ifndef STM32F4XX
	// change pin mode to digital input
	LCD_DATA_PORT->CRH = 0x47444444;
	LCD_DATA_PORT->CRL = 0x47444444;
#else

#endif

#ifndef STM32F4XX
    // change pin mode back to digital output
    LCD_DATA_PORT->CRH = 0x33333333;
    LCD_DATA_PORT->CRL = 0x33333333;
#else
#endif
   	LCD_RD_HIGH;

	return value;
}

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

    LCD_CS_LOW;
    LCD_RS_LOW;
    lld_lcdwrite(lcdReg);
    LCD_RS_HIGH;
    lcdRAM = lld_lcdReadData();

    LCD_CS_HIGH;

    return lcdRAM;
}

void lld_lcdWriteIndex(uint16_t lcdReg) {
	LCD_RS_LOW;

	lld_lcdwrite(lcdReg);

	LCD_RS_HIGH;
}

void lld_lcdWriteData(uint16_t lcdData) {
	lld_lcdwrite(lcdData);
}

void lld_lcdWriteReg(uint16_t lcdReg, uint16_t lcdRegValue) {
	LCD_CS_LOW;

	lld_lcdWriteIndex(lcdReg);
	lld_lcdWriteData(lcdRegValue);

	LCD_CS_HIGH;
}

static __inline void lld_lcdWriteStreamStart(void) {
	LCD_CS_LOW;
	lld_lcdWriteIndex(0x0022);
}

static __inline void lld_lcdWriteStreamStop(void) {
	LCD_CS_HIGH;
}

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

	for(i = 0; i < size; i++) {
		lld_lcdwrite(buffer[i]);
	}
}

__inline void lld_lcdReadStreamStart(void) {
	/* TODO */
}

__inline void lld_lcdReadStreamStop(void) {
	/* TODO */
}

__inline void lld_lcdReadStream(uint16_t *buffer, size_t size) {
	/* TODO */
}

#elif defined(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;
	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

void lld_lcdSetPowerMode(uint8_t powerMode) {
	/* TODO: implement */
}

void lld_lcdInit(void) {
	palSetPadMode(LCD_RST_GPIO, LCD_RST_PIN, PAL_MODE_OUTPUT_PUSHPULL | PAL_STM32_OSPEED_HIGHEST);
	// A Good idea to reset the module before using
	LCD_RST_LOW;
	s6d1121_delay(2);
	LCD_RST_HIGH;         // Hardware Reset
	s6d1121_delay(2);

#ifdef LCD_USE_GPIO
	// IO Default Configurations
	palSetPadMode(LCD_CS_GPIO, LCD_CS_PIN, PAL_MODE_OUTPUT_PUSHPULL | PAL_STM32_OSPEED_HIGHEST);
	palSetPadMode(LCD_WR_GPIO, LCD_WR_PIN, PAL_MODE_OUTPUT_PUSHPULL | PAL_STM32_OSPEED_HIGHEST);
	palSetPadMode(LCD_RD_GPIO, LCD_RD_PIN, PAL_MODE_OUTPUT_PUSHPULL | PAL_STM32_OSPEED_HIGHEST);
	palSetPadMode(LCD_RS_GPIO, LCD_RS_PIN, PAL_MODE_OUTPUT_PUSHPULL | PAL_STM32_OSPEED_HIGHEST);
	palSetPadMode(LCD_BL_GPIO, LCD_BL_PIN, PAL_MODE_OUTPUT_PUSHPULL | PAL_STM32_OSPEED_HIGHEST);

	palSetGroupMode(LCD_D0_GPIO, 0x0000000F, 0, PAL_MODE_OUTPUT_PUSHPULL | PAL_STM32_OSPEED_HIGHEST);
	palSetGroupMode(LCD_D4_GPIO, 0x0000FFF0, 0, PAL_MODE_OUTPUT_PUSHPULL | PAL_STM32_OSPEED_HIGHEST);

	LCD_CS_HIGH;
	LCD_RD_HIGH;
	LCD_WR_HIGH;
	LCD_BL_LOW;


#elif defined(LCD_USE_FSMC)
#if defined(STM32F1XX)
	/* FSMC setup. TODO: this only works for STM32F1 */
	rccEnableAHB(RCC_AHBENR_FSMCEN, 0);

	/* TODO: pin setup */
#elif defined(STM32F4XX)
	/* STM32F4 FSMC init */
	rccEnableAHB3(RCC_AHB3ENR_FSMCEN, 0);

	/* set pins to FSMC mode */
	IOBus busD = {GPIOD, (1 << 0) | (1 << 1) | (1 << 4) | (1 << 5) | (1 << 7) | (1 << 8) |
							(1 << 9) | (1 << 10) | (1 << 11) | (1 << 14) | (1 << 15), 0};

	IOBus busE = {GPIOE, (1 << 7) | (1 << 8) | (1 << 9) | (1 << 10) | (1 << 11) | (1 << 12) |
						(1 << 13) | (1 << 14) | (1 << 15), 0};

	palSetBusMode(&busD, PAL_MODE_ALTERNATE(12));
	palSetBusMode(&busE, PAL_MODE_ALTERNATE(12));
#else
#error "FSMC not implemented for this device"
#endif
	int FSMC_Bank = 0;
	/* FSMC timing */
	FSMC_Bank1->BTCR[FSMC_Bank+1] = (10) | (10 << 8) | (10 << 16);

	/* Bank1 NOR/SRAM control register configuration */
	FSMC_Bank1->BTCR[FSMC_Bank] = FSMC_BCR1_MWID_0 | FSMC_BCR1_WREN | FSMC_BCR1_MBKEN;
#endif

	lld_lcdWriteReg(0x11,0x2004);
	lld_lcdWriteReg(0x13,0xCC00);
	lld_lcdWriteReg(0x15,0x2600);
	lld_lcdWriteReg(0x14,0x252A);
	lld_lcdWriteReg(0x12,0x0033);
	lld_lcdWriteReg(0x13,0xCC04);

	s6d1121_delay(1);

	lld_lcdWriteReg(0x13,0xCC06);

	s6d1121_delay(1);

	lld_lcdWriteReg(0x13,0xCC4F);

	s6d1121_delay(1);

	lld_lcdWriteReg(0x13,0x674F);
	lld_lcdWriteReg(0x11,0x2003);

	s6d1121_delay(1);

	// Gamma Setting
	lld_lcdWriteReg(0x30,0x2609);
	lld_lcdWriteReg(0x31,0x242C);
	lld_lcdWriteReg(0x32,0x1F23);
	lld_lcdWriteReg(0x33,0x2425);
	lld_lcdWriteReg(0x34,0x2226);
	lld_lcdWriteReg(0x35,0x2523);
	lld_lcdWriteReg(0x36,0x1C1A);
	lld_lcdWriteReg(0x37,0x131D);
	lld_lcdWriteReg(0x38,0x0B11);
	lld_lcdWriteReg(0x39,0x1210);
	lld_lcdWriteReg(0x3A,0x1315);
	lld_lcdWriteReg(0x3B,0x3619);
	lld_lcdWriteReg(0x3C,0x0D00);
	lld_lcdWriteReg(0x3D,0x000D);

	lld_lcdWriteReg(0x16,0x0007);
	lld_lcdWriteReg(0x02,0x0013);
	lld_lcdWriteReg(0x03,0x0003);
	lld_lcdWriteReg(0x01,0x0127);

	s6d1121_delay(1);

	lld_lcdWriteReg(0x08,0x0303);
	lld_lcdWriteReg(0x0A,0x000B);
	lld_lcdWriteReg(0x0B,0x0003);
	lld_lcdWriteReg(0x0C,0x0000);
	lld_lcdWriteReg(0x41,0x0000);
	lld_lcdWriteReg(0x50,0x0000);
	lld_lcdWriteReg(0x60,0x0005);
	lld_lcdWriteReg(0x70,0x000B);
	lld_lcdWriteReg(0x71,0x0000);
	lld_lcdWriteReg(0x78,0x0000);
	lld_lcdWriteReg(0x7A,0x0000);
	lld_lcdWriteReg(0x79,0x0007);
	lld_lcdWriteReg(0x07,0x0051);

	s6d1121_delay(1);

	lld_lcdWriteReg(0x07,0x0053);
	lld_lcdWriteReg(0x79,0x0000);

	lld_lcdResetWindow();
}

void lld_lcdSetCursor(uint16_t x, uint16_t y) {
	/* R20h - 8 bit
	 * R21h - 9 bit
	 */
	switch(lcdGetOrientation()) {
		case portraitInv:
			lld_lcdWriteReg(0x0020, (SCREEN_WIDTH-1-x) & 0x00FF);
			lld_lcdWriteReg(0x0021, (SCREEN_HEIGHT-1-y) & 0x01FF);
			break;
		case portrait:
			lld_lcdWriteReg(0x0020, x & 0x00FF);
			lld_lcdWriteReg(0x0021, y & 0x01FF);
			break;
		case landscape:
			lld_lcdWriteReg(0x0020, y & 0x00FF);
			lld_lcdWriteReg(0x0021, x & 0x01FF);
			break;
		case landscapeInv:
			lld_lcdWriteReg(0x0020, (SCREEN_WIDTH - y - 1) & 0x00FF);
			lld_lcdWriteReg(0x0021, (SCREEN_HEIGHT - x - 1) & 0x01FF);
			break;
	}
}

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();

    lld_lcdResetWindow();
}

// Do not use now, will be fixed in future
void lld_lcdSetOrientation(uint8_t newOrientation) {
    orientation = newOrientation;

    switch(orientation) {
        case portrait:
        	lld_lcdWriteReg(0x0001,0x0127);
            lld_lcdWriteReg(0x03, 0b0011);
            lcd_height = SCREEN_HEIGHT;
            lcd_width = SCREEN_WIDTH;
            break;
        case landscape:
        	lld_lcdWriteReg(0x0001,0x0027);
        	lld_lcdWriteReg(0x0003, 0b1011);
            lcd_height = SCREEN_WIDTH;
            lcd_width = SCREEN_HEIGHT;
            break;
        case portraitInv:
        	lld_lcdWriteReg(0x0001,0x0127);
        	lld_lcdWriteReg(0x0003, 0b0000);
            lcd_height = SCREEN_HEIGHT;
            lcd_width = SCREEN_WIDTH;
            break;
        case landscapeInv:
        	lld_lcdWriteReg(0x0001,0x0027);
        	lld_lcdWriteReg(0x0003, 0b1000);
            lcd_height = SCREEN_WIDTH;
            lcd_width = SCREEN_HEIGHT;
            break;
    }
}

void lld_lcdResetWindow(void) {
	switch(lcdGetOrientation()) {
        case portrait:
        case portraitInv:
        	lld_lcdSetWindow(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
            break;
        case landscape:
        case landscapeInv:
        	lld_lcdSetWindow(0, 0, SCREEN_HEIGHT, SCREEN_WIDTH);
            break;
    }
}

void lld_lcdSetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
	/* HSA / HEA are 8 bit
	 * VSA / VEA are 9 bit
	 * use masks 0x00FF and 0x01FF to enforce this
	 */
	switch(lcdGetOrientation()) {
        case portrait:
            lld_lcdWriteReg(0x46, (((x1-1) & 0x00FF) << 8) | (x0 & 0x00FF));
            lld_lcdWriteReg(0x48, y0 & 0x01FF);
            lld_lcdWriteReg(0x47, (y1-1) & 0x01FF);
            break;
        case landscape:
            lld_lcdWriteReg(0x46, (((y1-1) & 0x00FF) << 8) | (y1 & 0x00FF));
            lld_lcdWriteReg(0x48, x0 & 0x01FF);
            lld_lcdWriteReg(0x47, (x1-1) & 0x01FF);
            break;
        case portraitInv:
            lld_lcdWriteReg(0x46, (((SCREEN_WIDTH-x0-1) & 0x00FF) << 8) | ((SCREEN_WIDTH - x1) & 0x00FF));
            lld_lcdWriteReg(0x48, (SCREEN_HEIGHT-y1) & 0x01FF);
            lld_lcdWriteReg(0x47, (SCREEN_HEIGHT-y0-1) & 0x01FF);
            break;
        case landscapeInv:
            lld_lcdWriteReg(0x46, (((SCREEN_WIDTH - y0 - 1) & 0x00FF) << 8) | ((SCREEN_WIDTH - y1) & 0x00FF));
            lld_lcdWriteReg(0x48, (SCREEN_HEIGHT - x1) & 0x01FF);
            lld_lcdWriteReg(0x47, (SCREEN_HEIGHT - x0 - 1) & 0x01FF);
            break;
    }

	lld_lcdSetCursor(x0, y0);
}

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();
}

// Do not use!
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);
}

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 */
/* TODO: test this */
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;

	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();
	}

	lld_lcdResetWindow();
}


#endif