/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2011 Bertrik Sikken
 *
 * This program 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 2
 * of the License, or (at your option) any later version.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/

#include "config.h"
#include "cpu.h"
#include "system.h"
#include "kernel.h"

#include "lcd.h"
#include "lcd-target.h"

/* the detected lcd type (0 or 1) */
static int lcd_type;

/* set by lcd_set_flip(): when true the panel is rotated 180 degrees via the
   controller's GRAM write direction (see lcd_set_flip and lcd_setup_rect) */
static bool lcd_flipped = false;

#ifdef HAVE_LCD_ENABLE
/* whether the lcd is currently enabled or not */
static bool lcd_enabled;
#endif

static void ssp_set_prescaler(unsigned int prescaler)
{
    int oldlevel = disable_interrupt_save(IRQ_FIQ_STATUS);
    /* must be on to write regs */
    bool ssp_enabled = bitset32(&CGU_PERI, CGU_SSP_CLOCK_ENABLE) &
                                           CGU_SSP_CLOCK_ENABLE;
    SSP_CPSR = prescaler;

    if (!ssp_enabled) /* put it back how we found it */
        bitclr32(&CGU_PERI, CGU_SSP_CLOCK_ENABLE);

    restore_irq(oldlevel);
}

/* initialises the host lcd hardware, returns the lcd type */
static int lcd_hw_init(void)
{
    /* configure SSP */
    bitset32(&CGU_PERI, CGU_SSP_CLOCK_ENABLE);
    ssp_set_prescaler(AS3525_SSP_PRESCALER);   /* OF = 0x8 */
    SSP_CR0 =   (0 << 8) |  /* SCR, serial clock rate divider = 1 */
                (1 << 7) |  /* SPH, phase = 1 */
                (1 << 6) |  /* SPO, polarity = 1 */
                (0 << 4) |  /* FRF, frame format = motorola SPI */
                (7 << 0);   /* DSS, data size select = 8 bits */
    SSP_CR1 =   (1 << 3) |  /* SOD, slave output disable = 1 */
                (0 << 2) |  /* MS, master/slave = master */
                (1 << 1) |  /* SSE, synchronous serial port enabled = true */
                (0 << 0);   /* LBM, loopback mode = normal */
    SSP_IMSC &= ~0xF;       /* disable interrupts */
    SSP_DMACR &= ~0x3;      /* disable DMA */

    /* GPIO A3 is ??? but needs to be set */
    GPIOA_DIR |= (1 << 3);
    GPIOA_PIN(3) = (1 << 3);

    /* configure GPIO B2 (lcd D/C#) as output */
    GPIOB_DIR |= (1<<2);

    /* configure GPIO B3 (lcd type detect) as input */
    GPIOB_DIR &= ~(1<<3);
    
    /* configure GPIO A5 (lcd reset#) as output and perform lcd reset */
    GPIOA_DIR |= (1 << 5);
    GPIOA_PIN(5) = 0;
    sleep(HZ * 50/1000);
    GPIOA_PIN(5) = (1 << 5);

    /* detect lcd type on GPIO B3 */    
    return GPIOB_PIN(3) ? 1 : 0;
}

/* writes a command byte to the LCD */
static void lcd_write_cmd(uint8_t byte)
{
    /* wait until not busy */
    while (SSP_SR & (1<<4));

    /* LCD command mode */
    GPIOB_PIN(2) = 0;
    
    /* write data */
    SSP_DATA = byte;
    
    /* wait until not busy */
    while (SSP_SR & (1<<4));

    /* LCD data mode */
    GPIOB_PIN(2) = (1 << 2);
}

/* writes a data byte to the LCD */
static void lcd_write_dat(uint8_t data)
{
    /* wait while transmit FIFO */
    while (!(SSP_SR & (1<<1)));   

    /* write data */
    SSP_DATA = data;
}

/* writes 2 data bytes to the LCD */
static void lcd_write_dat_word(uint8_t data1, uint8_t data2)
{
    lcd_write_dat(data1);
    lcd_write_dat(data2);
}

/* writes both a command and data value to the lcd */
static void lcd_write(uint8_t cmd, uint8_t data)
{
    lcd_write_cmd(cmd);
    lcd_write_dat(data);
}

/*  Initialises lcd type 0
 *  This appears to be a WiseChip OLED display controlled by a SEPS114A.
 */
static void lcd_init_type0(void) /* INIT_ATTR */
{
    lcd_write(0x01, 0x00);  /* SOFT_RESET */
    lcd_write(0x14, 0x01);  /* STANDBY_ON_OFF */
    sleep(1);   /* actually only 5 ms needed */

    lcd_write(0x14, 0x00);  /* STANDBY_ON_OFF */
    sleep(1);   /* actually only 5 ms needed */

    lcd_write(0x0F, 0x41);  /* ANALOG_CONTROL */
    lcd_write(0xEA, 0x0A);  /* ? */
    lcd_write(0xEB, 0x42);  /* ? */
    lcd_write(0x18, 0x08);  /* DISCHARGE_TIME */
    lcd_write(0x1A, 0x03);  /* OSC_ADJUST 3 = 95Hz*/
    lcd_write(0x48, 0x03);  /* ROW_OVERLAP */
    lcd_write(0x30, 0x00);  /* DISPLAY_X1 */
    lcd_write(0x31, 0x5F);  /* DISPLAY_X2 */
    lcd_write(0x32, 0x00);  /* DISPLAY_Y1 */
    lcd_write(0x33, 0x5F);  /* DISPLAY_Y2 */
    lcd_write(0x38, 0x00);  /* DISPLAYSTART_X */
    lcd_write(0x39, 0x00);  /* DISPLAYSTART_Y */
    lcd_write(0xE0, 0x10);  /* RGB_IF */
    lcd_write(0xE1, 0x00);  /* RGB_POL */
    lcd_write(0xE5, 0x80);  /* DISPLAY_MODE_CONTROL */
    lcd_write(0x0D, 0x00);  /* CPU_IF */
    lcd_write(0x1D, 0x01);  /* MEMORY_WRITE_READ */
    lcd_write(0x09, 0x00);  /* ROW_SCAN_DIRECTION */
    lcd_write(0x13, 0x00);  /* ROW_SCAN_MODE */
    lcd_write(0x16, 0x05);  /* PEAK_PULSE_DELAY */
    lcd_write(0x3A, 0x03);  /* PEAK_PULSE_WIDTH_R */
    lcd_write(0x3B, 0x03);  /* PEAK_PULSE_WIDTH_G */
    lcd_write(0x3C, 0x03);  /* PEAK_PULSE_WIDTH_B */
    lcd_write(0x3D, 0x45);  /* PRECHARGE_CURRENT_R */
    lcd_write(0x3E, 0x45);  /* PRECHARGE_CURRENT_G */
    lcd_write(0x3F, 0x45);  /* PRECHARGE_CURRENT_B */
    lcd_write(0x40, 0x62);  /* COLUMN_CURRENT_R */
    lcd_write(0x41, 0x3D);  /* COLUMN_CURRENT_G */
    lcd_write(0x42, 0x46);  /* COLUMN_CURRENT_B */
}

/* writes a table entry (for type 1 LCDs) */
static void lcd_write_nibbles(uint8_t val)
{
    lcd_write_dat_word((val >> 4) & 0x0F, (val >> 0) & 0x0F);
}

/* Initialises lcd type 1
 * This appears to be a Visionox OLED display, with a LDT LD7134 controller
 */
static void lcd_init_type1(void) /* INIT_ATTR */
{
    static const uint8_t curve[128] = {
        /* 5-bit curve */
        0, 5, 10, 15, 20, 25, 30, 35, 39, 43, 47, 51, 55, 59, 63, 67,
        71, 75, 79, 83, 87, 91, 95, 99, 103, 105, 109, 113, 117, 121, 123, 127,
        /* 6-bit curve */
        0, 2, 4, 6, 8, 10, 12, 16, 18, 24, 26, 28, 30, 32, 34, 36,
        38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68,
        70, 72, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102,
        104, 106, 108, 110, 112, 114, 116, 118, 120, 121, 122, 123, 124, 125, 126, 127,
        /* 5-bit curve */
        0, 5, 10, 15, 20, 25, 30, 35, 39, 43, 47, 51, 55, 59, 63, 67,
        71, 75, 79, 83, 87, 91, 93, 97, 101, 105, 109, 113, 117, 121, 124, 127
    };
    int i;

    lcd_write(0x02, 0x00); /* DDISP OFF */

    lcd_write_cmd(0x01);  /* SOFTRESET */

    lcd_write(0x03, 0x00); /* DSTBY ON */

    lcd_write(0x04, 0x02); /* Set OSC Control 2 = 90Hz*/

    lcd_write(0x05, 0x00); /* Write Direction 0x08 results in BGR colour */

    lcd_write(0x06, 0x00); /* Set Row Scan Direction */

    lcd_write(0x07, 0x00); /* Set Display Size */
    lcd_write_dat_word(0x00, 0x04);
    lcd_write_dat_word(0x1F, 0x00);
    lcd_write_dat_word(0x00, 0x05);
    lcd_write_dat(0x0F);

    lcd_write(0x08, 0x01); /* Set Interface Bus Type 1 = 8-bit*/

    lcd_write(0x09, 0x07); /* Set Masking Data */

    lcd_write_cmd(0x0A); /* Set Read/Write Box Data */
    lcd_write_nibbles(0);
    lcd_write_nibbles(LCD_WIDTH - 1);
    lcd_write_nibbles(0);
    lcd_write_nibbles(LCD_HEIGHT - 1);

    lcd_write(0x0B, 0x00); /* Set Display Start Address */
    lcd_write_dat_word(0x00, 0x00);
    lcd_write_dat(0x00);

    lcd_write_cmd(0x0E); /* Set Dot Matrix Current Level */
    lcd_write_nibbles(0x42);
    lcd_write_nibbles(0x25);
    lcd_write_nibbles(0x3F);

    lcd_write(0x0F, 0x0A); /* Set Dot Matrix Peak Current Level */
    lcd_write_dat_word(0x0A, 0x0A);

    lcd_write(0x1C, 0x08); /* Set Pre-Charge Width */

    lcd_write(0x1D, 0x00); /* Set Peak Pulse Width */
    lcd_write_dat_word(0x00, 0x00);

    lcd_write(0x1E, 0x05); /* Set Peak Pulse Delay */

    lcd_write(0x1F, 0x00); /* Set Row Scan Operation 0=Mode 1 : Default scan mode */

    lcd_write(0x30, 0x10); /* Set Internal Regulator for Row Scan*/

    lcd_write_cmd(0x3A); /* Set Gamma Correction Table */
    for (i = 0; i < 128; i++) {
        lcd_write_nibbles(curve[i]);
    }

    lcd_write_cmd(0x3B); /* Set Gamma Correction Table Initialize */

    lcd_write(0x3C, 0x00); /* Set VDD Selection */

    lcd_write(0x3D, 0x00); /* DMODE 0= 65K Color */
}

#ifdef HAVE_LCD_ENABLE
/* enables/disables the lcd */
void lcd_enable(bool on)
{
    if (on == lcd_enabled) {
        return;
    }

    if (lcd_type == 0) {
        if (on) {
            lcd_write(0x14, 0x00);  /* STANDBY OFF */
            lcd_write(0x02, 0x01);  /* DISP_ON */
            /* apply 180 degree flip via memory write direction (1Dh):
               MDIR1|MDIR0 = decrement both; normal is horizontal-decrement */
            lcd_write(0x1D, lcd_flipped ? 0x02 : 0x01);
        }
        else {
            lcd_write(0xD2, 0x05); /* SCREEN_SAVER_MODE 5=FADE_OUT*/
            lcd_write(0xD0, 0x80); /* SCREEN_SAVER_CONTROL */
            sleep(HZ/10);

            lcd_write(0x02, 0x00);
            lcd_write(0xD0, 0x00);
            lcd_write(0x14, 0x01);
        }
    }
    else {
        /* Type 1 */
        if (on) {
            lcd_write(0x03, 0x00); /* DSTBY OFF */

            lcd_write(0x02, 0x01); /* DDISP ON */

            /* apply 180 degree flip via graphic RAM writing direction (05h):
               D[2:0] = 011 starts from XE,YE (both axes reversed) */
            lcd_write(0x05, lcd_flipped ? 0x03 : 0x00);
        }
        else {
            lcd_write(0x02, 0x00);

            lcd_write(0x03, 0x01);

            lcd_write(0x14, 0x01); /* DSTBY ON */
        }
    }

    lcd_enabled = on;
}

/* returns true if the lcd is enabled */
bool lcd_active(void)
{
    return lcd_enabled;
}
#endif /* HAVE_LCD_ENABLE */

/* initialises the lcd */
void lcd_init_device(void) /* INIT_ATTR */
{
    lcd_type = lcd_hw_init();
    if (lcd_type == 0) {
        lcd_init_type0();
    }
    else {
        lcd_init_type1();
    }
    lcd_enable(true);
}

/* sets up the lcd to receive frame buffer data */
static void lcd_setup_rect(int x, int x_end, int y, int y_end)
{
    int fx, fx_end, fy;

    if (lcd_type == 0) {
        if (lcd_flipped) /* Type 0 LCD requires the flipped fx and fx_end ??*/
        {
        /* Mirror the window to the opposite corner; the reversed write
           direction set in lcd_enable() fills it back-to-front, drawing the
           framebuffer rotated 180 degrees with no per-pixel work. */
            fx = x;
            fx_end = x_end;
            fy = LCD_HEIGHT - 1 - y_end;
            y_end  = LCD_HEIGHT - 1 - y;
            y = fy;
        }
        else
        {
            fx = LCD_WIDTH - 1 - x_end;
            fx_end = LCD_WIDTH - 1 - x;
        }
        lcd_write(0x34, fx);    /* MEM_X1 */
        lcd_write(0x35, fx_end);/* MEM_X2 */
        lcd_write(0x36, y);     /* MEM_Y1 */
        lcd_write(0x37, y_end); /* MEM_Y2 */

        lcd_write_cmd(0x08);   /* DDRAM_DATA_ACCESS_PORT */
    }
    else {
        /* Type 1*/
        if (lcd_flipped) {
        /* Mirror the window to the opposite corner; the reversed write
           direction set in lcd_enable() fills it back-to-front, drawing the
           framebuffer rotated 180 degrees with no per-pixel work. */
            fx = LCD_WIDTH  - 1 - x_end;
            x_end  = LCD_WIDTH  - 1 - x;
            x = fx;
            fy = LCD_HEIGHT - 1 - y_end;
            y_end  = LCD_HEIGHT - 1 - y;
            y = fy;
        }

        lcd_write_cmd(0x0A); /* Set Read/Write Box Data */
        lcd_write_nibbles(x);
        lcd_write_nibbles(x_end);
        lcd_write_nibbles(y);
        lcd_write_nibbles(y_end);

        lcd_write_cmd(0x0C); /* Read/Write Display Data */
    }
}

/* sets the brightness of the OLED */
void oled_brightness(int brightness)
{
    int r, g, b;

    if (lcd_type == 0) {
        r = 2 + 16*brightness;
        g = 1 + 10*brightness;
        b = 1 + (23*brightness)/2;
    
        lcd_write(0x40, r); /* COLUMN_CURRENT_R */
        lcd_write(0x41, g); /* COLUMN_CURRENT_G */
        lcd_write(0x42, b); /* COLUMN_CURRENT_B */
    }
    else {
        r = 6 + 10*brightness;
        g = 1 + 6*brightness;
        b = 3 + 10*brightness;
    
        lcd_write_cmd(0x0E); /* Set Dot Matrix Current Level */
        lcd_write_nibbles(r);
        lcd_write_nibbles(g);
        lcd_write_nibbles(b);
    }
}

/* Writes framebuffer data */
void lcd_write_data(const fb_data *data, int count)
{
    fb_data pixel;

    while (count--) {
        pixel = *data++;
        lcd_write_dat_word((pixel >> 8) & 0xFF, (pixel >> 0) & 0xFF);
    }
}

/* Rotate the display 180 degrees (the flip_display setting). Both panel
   controllers do this in hardware by reversing the GRAM write direction (set in
   lcd_enable) so the address counter walks the framebuffer backwards;
   lcd_setup_rect mirrors the write window to match. Cycle the panel off/on here
   so a change (e.g. toggled from the menu) takes effect immediately. */
void lcd_set_flip(bool yesno)
{
    if (yesno == lcd_flipped)
        return;
    lcd_flipped = yesno;

    if (lcd_enabled) {
        lcd_enable(false);
        lcd_enable(true);
    }
}

/* Updates a fraction of the display. */
void lcd_update_rect(int x, int y, int width, int height)
{
    int row;
    int x_end = x + width;
    int y_end = y + height;

    /* check rectangle */
    if ((x >= LCD_WIDTH) || (x_end <= 0) || (y >= LCD_HEIGHT) || (y_end <= 0)) {
        /* rectangle is outside visible display, do nothing */
        return;
    }

#if 0 /* no longer needed unless it turns out that some devices are wired backwards */
    /* update entire horizontal strip for display type 0 (wisechip) */
    if (lcd_type == 0) {
        x = 0;
        x_end = 96;
    }
#endif

    /* correct rectangle (if necessary) */
    if (x < 0) {
        x = 0;
    }
    if (x_end > LCD_WIDTH) {
        x_end = LCD_WIDTH;
    }
    if (y < 0) {
        y = 0;
    }
    if (y_end > LCD_HEIGHT) {
        y_end = LCD_HEIGHT;
    }
    width = x_end - x;

    /* setup GRAM write window */
    lcd_setup_rect(x, x_end - 1, y, y_end - 1);

    void* (*fbaddr)(int x, int y) = FB_CURRENTVP_BUFFER->get_address_fn;
    /* write to GRAM */
    for (row = y; row < y_end; row++) {
        lcd_write_data(fbaddr(x,row), width);
    }
}

/* updates the entire lcd */
void lcd_update(void)
{
    lcd_update_rect(0, 0, LCD_WIDTH, LCD_HEIGHT);
}

