/**
 * @file ILI9341.c
 *
 * ILI9341.pdf [ILI9341_DS_V1.13_20110805]
 *
 * [references]
 * - https://www.newhavendisplay.com/app_notes/ILI9341.pdf
 * - Linux Source [v5.9-rc4] "drivers/staging/fbtft/fb_ili9341.c"
 * - https://github.com/adafruit/Adafruit_ILI9341/blob/master/Adafruit_ILI9341.cpp
 * - https://os.mbed.com/users/dreschpe/code/SPI_TFT_ILI9341
 *
 */

/*********************
 *      INCLUDES
 *********************/
#include "ILI9341.h"
#if USE_ILI9341 != 0

#include <stdio.h>
#include <stdbool.h>
#include LV_DRV_DISP_INCLUDE
#include LV_DRV_DELAY_INCLUDE

/*********************
 *      DEFINES
 *********************/
#define ILI9341_CMD_MODE    0
#define ILI9341_DATA_MODE   1

#define ILI9341_TFTWIDTH    240
#define ILI9341_TFTHEIGHT   320

/* Level 1 Commands -------------- [section] Description */

#define ILI9341_NOP         0x00 /* [8.2.1 ] No Operation / Terminate Frame Memory Write */
#define ILI9341_SWRESET     0x01 /* [8.2.2 ] Software Reset */
#define ILI9341_RDDIDIF     0x04 /* [8.2.3 ] Read Display Identification Information */
#define ILI9341_RDDST       0x09 /* [8.2.4 ] Read Display Status */
#define ILI9341_RDDPM       0x0A /* [8.2.5 ] Read Display Power Mode */
#define ILI9341_RDDMADCTL   0x0B /* [8.2.6 ] Read Display MADCTL */
#define ILI9341_RDDCOLMOD   0x0C /* [8.2.7 ] Read Display Pixel Format */
#define ILI9341_RDDIM       0x0D /* [8.2.8 ] Read Display Image Mode */
#define ILI9341_RDDSM       0x0E /* [8.2.9 ] Read Display Signal Mode */
#define ILI9341_RDDSDR      0x0F /* [8.2.10] Read Display Self-Diagnostic Result */
#define ILI9341_SLPIN       0x10 /* [8.2.11] Enter Sleep Mode */
#define ILI9341_SLPOUT      0x11 /* [8.2.12] Leave Sleep Mode */
#define ILI9341_PTLON       0x12 /* [8.2.13] Partial Display Mode ON */
#define ILI9341_NORON       0x13 /* [8.2.14] Normal Display Mode ON */
#define ILI9341_DINVOFF     0x20 /* [8.2.15] Display Inversion OFF */
#define ILI9341_DINVON      0x21 /* [8.2.16] Display Inversion ON */
#define ILI9341_GAMSET      0x26 /* [8.2.17] Gamma Set */
#define ILI9341_DISPOFF     0x28 /* [8.2.18] Display OFF*/
#define ILI9341_DISPON      0x29 /* [8.2.19] Display ON*/
#define ILI9341_CASET       0x2A /* [8.2.20] Column Address Set */
#define ILI9341_PASET       0x2B /* [8.2.21] Page Address Set */
#define ILI9341_RAMWR       0x2C /* [8.2.22] Memory Write */
#define ILI9341_RGBSET      0x2D /* [8.2.23] Color Set (LUT for 16-bit to 18-bit color depth conversion) */
#define ILI9341_RAMRD       0x2E /* [8.2.24] Memory Read */
#define ILI9341_PTLAR       0x30 /* [8.2.25] Partial Area */
#define ILI9341_VSCRDEF     0x33 /* [8.2.26] Veritcal Scrolling Definition */
#define ILI9341_TEOFF       0x34 /* [8.2.27] Tearing Effect Line OFF */
#define ILI9341_TEON        0x35 /* [8.2.28] Tearing Effect Line ON */
#define ILI9341_MADCTL      0x36 /* [8.2.29] Memory Access Control */
#define     MADCTL_MY       0x80 /*          MY row address order */
#define     MADCTL_MX       0x40 /*          MX column address order */
#define     MADCTL_MV       0x20 /*          MV row / column exchange */
#define     MADCTL_ML       0x10 /*          ML vertical refresh order */
#define     MADCTL_MH       0x04 /*          MH horizontal refresh order */
#define     MADCTL_RGB      0x00 /*          RGB Order [default] */
#define     MADCTL_BGR      0x08 /*          BGR Order */
#define ILI9341_VSCRSADD    0x37 /* [8.2.30] Vertical Scrolling Start Address */
#define ILI9341_IDMOFF      0x38 /* [8.2.31] Idle Mode OFF */
#define ILI9341_IDMON       0x39 /* [8.2.32] Idle Mode ON */
#define ILI9341_PIXSET      0x3A /* [8.2.33] Pixel Format Set */
#define ILI9341_WRMEMCONT   0x3C /* [8.2.34] Write Memory Continue */
#define ILI9341_RDMEMCONT   0x3E /* [8.2.35] Read Memory Continue */
#define ILI9341_SETSCANTE   0x44 /* [8.2.36] Set Tear Scanline */
#define ILI9341_GETSCAN     0x45 /* [8.2.37] Get Scanline */
#define ILI9341_WRDISBV     0x51 /* [8.2.38] Write Display Brightness Value */
#define ILI9341_RDDISBV     0x52 /* [8.2.39] Read Display Brightness Value */
#define ILI9341_WRCTRLD     0x53 /* [8.2.40] Write Control Display */
#define ILI9341_RDCTRLD     0x54 /* [8.2.41] Read Control Display */
#define ILI9341_WRCABC      0x55 /* [8.2.42] Write Content Adaptive Brightness Control Value */
#define ILI9341_RDCABC      0x56 /* [8.2.43] Read Content Adaptive Brightness Control Value */
#define ILI9341_WRCABCMIN   0x5E /* [8.2.44] Write CABC Minimum Brightness */
#define ILI9341_RDCABCMIN   0x5F /* [8.2.45] Read CABC Minimum Brightness */
#define ILI9341_RDID1       0xDA /* [8.2.46] Read ID1 - Manufacturer ID (user) */
#define ILI9341_RDID2       0xDB /* [8.2.47] Read ID2 - Module/Driver version (supplier) */
#define ILI9341_RDID3       0xDC /* [8.2.48] Read ID3 - Module/Driver version (user) */

/* Level 2 Commands -------------- [section] Description */

#define ILI9341_IFMODE      0xB0 /* [8.3.1 ] Interface Mode Control */
#define ILI9341_FRMCTR1     0xB1 /* [8.3.2 ] Frame Rate Control (In Normal Mode/Full Colors) */
#define ILI9341_FRMCTR2     0xB2 /* [8.3.3 ] Frame Rate Control (In Idle Mode/8 colors) */
#define ILI9341_FRMCTR3     0xB3 /* [8.3.4 ] Frame Rate control (In Partial Mode/Full Colors) */
#define ILI9341_INVTR       0xB4 /* [8.3.5 ] Display Inversion Control */
#define ILI9341_PRCTR       0xB5 /* [8.3.6 ] Blanking Porch Control */
#define ILI9341_DISCTRL     0xB6 /* [8.3.7 ] Display Function Control */
#define ILI9341_ETMOD       0xB7 /* [8.3.8 ] Entry Mode Set */
#define ILI9341_BLCTRL1     0xB8 /* [8.3.9 ] Backlight Control 1 - Grayscale Histogram UI mode */
#define ILI9341_BLCTRL2     0xB9 /* [8.3.10] Backlight Control 2 - Grayscale Histogram still picture mode */
#define ILI9341_BLCTRL3     0xBA /* [8.3.11] Backlight Control 3 - Grayscale Thresholds UI mode */
#define ILI9341_BLCTRL4     0xBB /* [8.3.12] Backlight Control 4 - Grayscale Thresholds still picture mode */
#define ILI9341_BLCTRL5     0xBC /* [8.3.13] Backlight Control 5 - Brightness Transition time */
#define ILI9341_BLCTRL7     0xBE /* [8.3.14] Backlight Control 7 - PWM Frequency */
#define ILI9341_BLCTRL8     0xBF /* [8.3.15] Backlight Control 8 - ON/OFF + PWM Polarity*/
#define ILI9341_PWCTRL1     0xC0 /* [8.3.16] Power Control 1 - GVDD */
#define ILI9341_PWCTRL2     0xC1 /* [8.3.17] Power Control 2 - step-up factor for operating voltage */
#define ILI9341_VMCTRL1     0xC5 /* [8.3.18] VCOM Control 1 - Set VCOMH and VCOML */
#define ILI9341_VMCTRL2     0xC7 /* [8.3.19] VCOM Control 2 - VCOM offset voltage */
#define ILI9341_NVMWR       0xD0 /* [8.3.20] NV Memory Write */
#define ILI9341_NVMPKEY     0xD1 /* [8.3.21] NV Memory Protection Key */
#define ILI9341_RDNVM       0xD2 /* [8.3.22] NV Memory Status Read */
#define ILI9341_RDID4       0xD3 /* [8.3.23] Read ID4 - IC Device Code */
#define ILI9341_PGAMCTRL    0xE0 /* [8.3.24] Positive Gamma Control */
#define ILI9341_NGAMCTRL    0xE1 /* [8.3.25] Negative Gamma Correction */
#define ILI9341_DGAMCTRL1   0xE2 /* [8.3.26] Digital Gamma Control 1 */
#define ILI9341_DGAMCTRL2   0xE3 /* [8.3.27] Digital Gamma Control 2 */
#define ILI9341_IFCTL       0xF6 /* [8.3.28] 16bits Data Format Selection */

/* Extended Commands --------------- [section] Description*/

#define ILI9341_PWCTRLA       0xCB /* [8.4.1] Power control A */
#define ILI9341_PWCTRLB       0xCF /* [8.4.2] Power control B */
#define ILI9341_TIMECTRLA_INT 0xE8 /* [8.4.3] Internal Clock Driver timing control A */
#define ILI9341_TIMECTRLA_EXT 0xE9 /* [8.4.4] External Clock Driver timing control A */
#define ILI9341_TIMECTRLB     0xEA /* [8.4.5] Driver timing control B (gate driver timing control) */
#define ILI9341_PWSEQCTRL     0xED /* [8.4.6] Power on sequence control */
#define ILI9341_GAM3CTRL      0xF2 /* [8.4.7] Enable 3 gamma control */
#define ILI9341_PUMPRATIO     0xF7 /* [8.4.8] Pump ratio control */

/**********************
 *      TYPEDEFS
 **********************/

/**********************
 *  STATIC PROTOTYPES
 **********************/
static inline void ili9341_write(int mode, uint8_t data);
static inline void ili9341_write_array(int mode, uint8_t *data, uint16_t len);

/**********************
 *  STATIC VARIABLES
 **********************/

/**********************
 *      MACROS
 **********************/

/**********************
 *   GLOBAL FUNCTIONS
 **********************/

/**
 * Initialize the ILI9341 display controller
 */
void ili9341_init(void)
{
    uint8_t data[15];

    /* hardware reset */
    LV_DRV_DISP_SPI_CS(1);
    LV_DRV_DISP_CMD_DATA(ILI9341_DATA_MODE);
    LV_DRV_DISP_RST(0);
    LV_DRV_DELAY_US(50);
    LV_DRV_DISP_RST(1);
    LV_DRV_DELAY_MS(5);

    /* software reset */
    ili9341_write(ILI9341_CMD_MODE, ILI9341_SWRESET);
    LV_DRV_DELAY_MS(5);
    ili9341_write(ILI9341_CMD_MODE, ILI9341_DISPOFF);

    /* startup sequence */
    ili9341_write(ILI9341_CMD_MODE, ILI9341_PWCTRLB);
    data[0] = 0x00;
    data[1] = 0x83;
    data[2] = 0x30;
    ili9341_write_array(ILI9341_DATA_MODE, data, 3);

    ili9341_write(ILI9341_CMD_MODE, ILI9341_PWSEQCTRL);
    data[0] = 0x64;
    data[1] = 0x03;
    data[2] = 0x12;
    data[3] = 0x81;
    ili9341_write_array(ILI9341_DATA_MODE, data, 4);

    ili9341_write(ILI9341_CMD_MODE, ILI9341_TIMECTRLA_INT);
    data[0] = 0x85;
    data[1] = 0x01;
    data[2] = 0x79;
    ili9341_write_array(ILI9341_DATA_MODE, data, 3);

    ili9341_write(ILI9341_CMD_MODE, ILI9341_PWCTRLA);
    data[0] = 0x39;
    data[1] = 0x2c;
    data[2] = 0x00;
    data[3] = 0x34;
    data[4] = 0x02;
    ili9341_write_array(ILI9341_DATA_MODE, data, 5);

    ili9341_write(ILI9341_CMD_MODE, ILI9341_PUMPRATIO);
    ili9341_write(ILI9341_DATA_MODE, 0x20);

    ili9341_write(ILI9341_CMD_MODE, ILI9341_TIMECTRLB);
    data[0] = 0x00;
    data[1] = 0x00;
    ili9341_write_array(ILI9341_DATA_MODE, data, 2);

    /* power control */
    ili9341_write(ILI9341_CMD_MODE, ILI9341_PWCTRL1);
    ili9341_write(ILI9341_DATA_MODE, 0x26);

    ili9341_write(ILI9341_CMD_MODE, ILI9341_PWCTRL2);
    ili9341_write(ILI9341_DATA_MODE, 0x11);

    /* VCOM */
    ili9341_write(ILI9341_CMD_MODE, ILI9341_VMCTRL1);
    data[0] = 0x35;
    data[1] = 0x3e;
    ili9341_write_array(ILI9341_DATA_MODE, data, 2);

    ili9341_write(ILI9341_CMD_MODE, ILI9341_VMCTRL2);
    ili9341_write(ILI9341_DATA_MODE, 0xbe);

    /* set orientation */
    ili9341_rotate(0, ILI9341_BGR);

    /* 16 bit pixel */
    ili9341_write(ILI9341_CMD_MODE, ILI9341_PIXSET);
    ili9341_write(ILI9341_DATA_MODE, 0x55);

    /* frame rate */
    ili9341_write(ILI9341_CMD_MODE, ILI9341_FRMCTR1);
    data[0] = 0x00;
    data[1] = 0x1b;
    ili9341_write_array(ILI9341_DATA_MODE, data, 2);

#if ILI9341_GAMMA
    /* gamma curve set */
    ili9341_write(ILI9341_CMD_MODE, ILI9341_GAMSET);
    ili9341_write(ILI9341_DATA_MODE, 0x01);

    /* positive gamma correction */
    ili9341_write(ILI9341_CMD_MODE, ILI9341_PGAMCTRL);
    data[0]  = 0x1f;
    data[1]  = 0x1a;
    data[2]  = 0x18;
    data[3]  = 0x0a;
    data[4]  = 0x0f;
    data[5]  = 0x06;
    data[6]  = 0x45;
    data[7]  = 0x87;
    data[8]  = 0x32;
    data[9]  = 0x0a;
    data[10] = 0x07;
    data[11] = 0x02;
    data[12] = 0x07;
    data[13] = 0x05;
    data[14] = 0x00;
    ili9341_write_array(ILI9341_DATA_MODE, data, 15);

    /* negative gamma correction */
    ili9341_write(ILI9341_CMD_MODE, ILI9341_NGAMCTRL);
    data[0]  = 0x00;
    data[1]  = 0x25;
    data[2]  = 0x27;
    data[3]  = 0x05;
    data[4]  = 0x10;
    data[5]  = 0x09;
    data[6]  = 0x3a;
    data[7]  = 0x78;
    data[8]  = 0x4d;
    data[9]  = 0x05;
    data[10] = 0x18;
    data[11] = 0x0d;
    data[12] = 0x38;
    data[13] = 0x3a;
    data[14] = 0x1f;
    ili9341_write_array(ILI9341_DATA_MODE, data, 15);
#endif

    /* window horizontal */
    ili9341_write(ILI9341_CMD_MODE, ILI9341_CASET);
    data[0] = 0;
    data[1] = 0;
    data[2] = (ILI9341_HOR_RES - 1) >> 8;
    data[3] = (ILI9341_HOR_RES - 1);
    ili9341_write_array(ILI9341_DATA_MODE, data, 4);

    /* window vertical */
    ili9341_write(ILI9341_CMD_MODE, ILI9341_PASET);
    data[0] = 0;
    data[1] = 0;
    data[2] = (ILI9341_VER_RES - 1) >> 8;
    data[3] = (ILI9341_VER_RES - 1);
    ili9341_write_array(ILI9341_DATA_MODE, data, 4);

    ili9341_write(ILI9341_CMD_MODE, ILI9341_RAMWR);

#if ILI9341_TEARING
    /* tearing effect off */
    ili9341_write(ILI9341_CMD_MODE, ILI9341_TEOFF);

    /* tearing effect on */
    ili9341_write(ILI9341_CMD_MODE, ILI9341_TEON);
#endif

    /* entry mode set */
    ili9341_write(ILI9341_CMD_MODE, ILI9341_ETMOD);
    ili9341_write(ILI9341_DATA_MODE, 0x07);

    /* display function control */
    ili9341_write(ILI9341_CMD_MODE, ILI9341_DISCTRL);
    data[0] = 0x0a;
    data[1] = 0x82;
    data[2] = 0x27;
    data[3] = 0x00;
    ili9341_write_array(ILI9341_DATA_MODE, data, 4);

    /* exit sleep mode */
    ili9341_write(ILI9341_CMD_MODE, ILI9341_SLPOUT);

    LV_DRV_DELAY_MS(100);

    /* display on */
    ili9341_write(ILI9341_CMD_MODE, ILI9341_DISPON);

    LV_DRV_DELAY_MS(20);
}

void ili9341_flush(lv_disp_drv_t * drv, const lv_area_t * area, const lv_color_t * color_p)
{
    if(area->x2 < 0 || area->y2 < 0 || area->x1 > (ILI9341_HOR_RES - 1) || area->y1 > (ILI9341_VER_RES - 1)) {
        lv_disp_flush_ready(drv);
        return;
    }

    /* Truncate the area to the screen */
    int32_t act_x1 = area->x1 < 0 ? 0 : area->x1;
    int32_t act_y1 = area->y1 < 0 ? 0 : area->y1;
    int32_t act_x2 = area->x2 > ILI9341_HOR_RES - 1 ? ILI9341_HOR_RES - 1 : area->x2;
    int32_t act_y2 = area->y2 > ILI9341_VER_RES - 1 ? ILI9341_VER_RES - 1 : area->y2;

    int32_t y;
    uint8_t data[4];
    int32_t len = len = (act_x2 - act_x1 + 1) * 2;
    lv_coord_t w = (area->x2 - area->x1) + 1;

    /* window horizontal */
    ili9341_write(ILI9341_CMD_MODE, ILI9341_CASET);
    data[0] = act_x1 >> 8;
    data[1] = act_x1;
    data[2] = act_x2 >> 8;
    data[3] = act_x2;
    ili9341_write_array(ILI9341_DATA_MODE, data, 4);

    /* window vertical */
    ili9341_write(ILI9341_CMD_MODE,  ILI9341_PASET);
    data[0] = act_y1 >> 8;
    data[1] = act_y1;
    data[2] = act_y2 >> 8;
    data[3] = act_y2;
    ili9341_write_array(ILI9341_DATA_MODE, data, 4);

    ili9341_write(ILI9341_CMD_MODE, ILI9341_RAMWR);

    for(y = act_y1; y <= act_y2; y++) {
        ili9341_write_array(ILI9341_DATA_MODE, (uint8_t *)color_p, len);
        color_p += w;
    }

    lv_disp_flush_ready(drv);
}

void ili9341_rotate(int degrees, bool bgr)
{
    uint8_t color_order = MADCTL_RGB;

    if(bgr)
        color_order = MADCTL_BGR;

    ili9341_write(ILI9341_CMD_MODE, ILI9341_MADCTL);

    switch(degrees) {
    case 270:
        ili9341_write(ILI9341_DATA_MODE, MADCTL_MV | color_order);
        break;
    case 180:
        ili9341_write(ILI9341_DATA_MODE, MADCTL_MY | color_order);
        break;
    case 90:
        ili9341_write(ILI9341_DATA_MODE, MADCTL_MX | MADCTL_MY | MADCTL_MV | color_order);
        break;
    case 0:
        /* fall-through */
    default:
        ili9341_write(ILI9341_DATA_MODE, MADCTL_MX | color_order);
        break;
    }
}

/**********************
 *   STATIC FUNCTIONS
 **********************/

/**
 * Write byte
 * @param mode sets command or data mode for write
 * @param byte the byte to write
 */
static inline void ili9341_write(int mode, uint8_t data)
{
    LV_DRV_DISP_CMD_DATA(mode);
    LV_DRV_DISP_SPI_WR_BYTE(data);
}

/**
 * Write byte array
 * @param mode sets command or data mode for write
 * @param data the byte array to write
 * @param len the length of the byte array
 */
static inline void ili9341_write_array(int mode, uint8_t *data, uint16_t len)
{
    LV_DRV_DISP_CMD_DATA(mode);
    LV_DRV_DISP_SPI_WR_ARRAY(data, len);
}

#endif
