/*
 * This file is subject to the terms of the GFX License. If a copy of
 * the license was not distributed with this file, you can obtain one at:
 *
 *              http://ugfx.org/license.html
 */

#include "gfx.h"

#if GFX_USE_GDISP

#    define GDISP_DRIVER_VMT GDISPVMT_ST7565_QMK
#    include "gdisp_lld_config.h"
#    include "src/gdisp/gdisp_driver.h"

#    include "board_st7565.h"

/*===========================================================================*/
/* Driver local definitions.                                                 */
/*===========================================================================*/

#    ifndef GDISP_SCREEN_HEIGHT
#        define GDISP_SCREEN_HEIGHT LCD_HEIGHT
#    endif
#    ifndef GDISP_SCREEN_WIDTH
#        define GDISP_SCREEN_WIDTH LCD_WIDTH
#    endif
#    ifndef GDISP_INITIAL_CONTRAST
#        define GDISP_INITIAL_CONTRAST 35
#    endif
#    ifndef GDISP_INITIAL_BACKLIGHT
#        define GDISP_INITIAL_BACKLIGHT 100
#    endif

#    define GDISP_FLG_NEEDFLUSH (GDISP_FLG_DRIVER << 0)

#    include "st7565.h"

/*===========================================================================*/
/* Driver config defaults for backward compatibility.                        */
/*===========================================================================*/
#    ifndef ST7565_LCD_BIAS
#        define ST7565_LCD_BIAS ST7565_LCD_BIAS_7
#    endif
#    ifndef ST7565_ADC
#        define ST7565_ADC ST7565_ADC_NORMAL
#    endif
#    ifndef ST7565_COM_SCAN
#        define ST7565_COM_SCAN ST7565_COM_SCAN_INC
#    endif
#    ifndef ST7565_PAGE_ORDER
#        define ST7565_PAGE_ORDER 0, 1, 2, 3
#    endif

/*===========================================================================*/
/* Driver local functions.                                                   */
/*===========================================================================*/

typedef struct {
    bool_t  buffer2;
    uint8_t data_pos;
    uint8_t data[16];
    uint8_t ram[GDISP_SCREEN_HEIGHT * GDISP_SCREEN_WIDTH / 8];
} PrivData;

// Some common routines and macros
#    define PRIV(g) ((PrivData *)g->priv)
#    define RAM(g) (PRIV(g)->ram)

static GFXINLINE void write_cmd(GDisplay *g, uint8_t cmd) { PRIV(g)->data[PRIV(g)->data_pos++] = cmd; }

static GFXINLINE void flush_cmd(GDisplay *g) {
    write_data(g, PRIV(g)->data, PRIV(g)->data_pos);
    PRIV(g)->data_pos = 0;
}

#    define write_cmd2(g, cmd1, cmd2) \
        {                             \
            write_cmd(g, cmd1);       \
            write_cmd(g, cmd2);       \
        }
#    define write_cmd3(g, cmd1, cmd2, cmd3) \
        {                                   \
            write_cmd(g, cmd1);             \
            write_cmd(g, cmd2);             \
            write_cmd(g, cmd3);             \
        }

// Some common routines and macros
#    define delay(us) gfxSleepMicroseconds(us)
#    define delay_ms(ms) gfxSleepMilliseconds(ms)

#    define xyaddr(x, y) ((x) + ((y) >> 3) * GDISP_SCREEN_WIDTH)
#    define xybit(y) (1 << ((y)&7))

/*===========================================================================*/
/* Driver exported functions.                                                */
/*===========================================================================*/

/*
 * As this controller can't update on a pixel boundary we need to maintain the
 * the entire display surface in memory so that we can do the necessary bit
 * operations. Fortunately it is a small display in monochrome.
 * 64 * 128 / 8 = 1024 bytes.
 */

LLDSPEC bool_t gdisp_lld_init(GDisplay *g) {
    // The private area is the display surface.
    g->priv           = gfxAlloc(sizeof(PrivData));
    PRIV(g)->buffer2  = false;
    PRIV(g)->data_pos = 0;

    // Initialise the board interface
    init_board(g);

    // Hardware reset
    setpin_reset(g, TRUE);
    gfxSleepMilliseconds(20);
    setpin_reset(g, FALSE);
    gfxSleepMilliseconds(20);
    acquire_bus(g);
    enter_cmd_mode(g);

    write_cmd(g, ST7565_RESET);
    write_cmd(g, ST7565_LCD_BIAS);
    write_cmd(g, ST7565_ADC);
    write_cmd(g, ST7565_COM_SCAN);

    write_cmd(g, ST7565_RESISTOR_RATIO | 0x1);
    write_cmd2(g, ST7565_CONTRAST, GDISP_INITIAL_CONTRAST);

    // turn on internal power supply (VC=1, VR=1, VF=1)
    write_cmd(g, ST7565_POWER_CONTROL | 0x07);

    write_cmd(g, ST7565_INVERT_DISPLAY);
    write_cmd(g, ST7565_ALLON_NORMAL);

    write_cmd(g, ST7565_START_LINE | 0);
    write_cmd(g, ST7565_RMW);
    flush_cmd(g);

    // Finish Init
    post_init_board(g);

    // Release the bus
    release_bus(g);

    /* Initialise the GDISP structure */
    g->g.Width       = GDISP_SCREEN_WIDTH;
    g->g.Height      = GDISP_SCREEN_HEIGHT;
    g->g.Orientation = GDISP_ROTATE_0;
    g->g.Powermode   = powerOff;
    g->g.Backlight   = GDISP_INITIAL_BACKLIGHT;
    g->g.Contrast    = GDISP_INITIAL_CONTRAST;
    return TRUE;
}

#    if GDISP_HARDWARE_FLUSH
LLDSPEC void gdisp_lld_flush(GDisplay *g) {
    unsigned p;

    // Don't flush if we don't need it.
    if (!(g->flags & GDISP_FLG_NEEDFLUSH)) return;

    acquire_bus(g);
    enter_cmd_mode(g);
    unsigned dstOffset = (PRIV(g)->buffer2 ? 4 : 0);
    for (p = 0; p < 4; p++) {
        write_cmd(g, ST7565_PAGE | (p + dstOffset));
        write_cmd(g, ST7565_COLUMN_MSB | 0);
        write_cmd(g, ST7565_COLUMN_LSB | 0);
        write_cmd(g, ST7565_RMW);
        flush_cmd(g);
        enter_data_mode(g);
        write_data(g, RAM(g) + (p * GDISP_SCREEN_WIDTH), GDISP_SCREEN_WIDTH);
        enter_cmd_mode(g);
    }
    unsigned line = (PRIV(g)->buffer2 ? 32 : 0);
    write_cmd(g, ST7565_START_LINE | line);
    flush_cmd(g);
    PRIV(g)->buffer2 = !PRIV(g)->buffer2;
    release_bus(g);

    g->flags &= ~GDISP_FLG_NEEDFLUSH;
}
#    endif

#    if GDISP_HARDWARE_DRAWPIXEL
LLDSPEC void gdisp_lld_draw_pixel(GDisplay *g) {
    coord_t x, y;

    switch (g->g.Orientation) {
        default:
        case GDISP_ROTATE_0:
            x = g->p.x;
            y = g->p.y;
            break;
        case GDISP_ROTATE_90:
            x = g->p.y;
            y = GDISP_SCREEN_HEIGHT - 1 - g->p.x;
            break;
        case GDISP_ROTATE_180:
            x = GDISP_SCREEN_WIDTH - 1 - g->p.x;
            y = GDISP_SCREEN_HEIGHT - 1 - g->p.y;
            break;
        case GDISP_ROTATE_270:
            x = GDISP_SCREEN_HEIGHT - 1 - g->p.y;
            y = g->p.x;
            break;
    }
    if (gdispColor2Native(g->p.color) != Black)
        RAM(g)[xyaddr(x, y)] |= xybit(y);
    else
        RAM(g)[xyaddr(x, y)] &= ~xybit(y);
    g->flags |= GDISP_FLG_NEEDFLUSH;
}
#    endif

#    if GDISP_HARDWARE_PIXELREAD
LLDSPEC color_t gdisp_lld_get_pixel_color(GDisplay *g) {
    coord_t x, y;

    switch (g->g.Orientation) {
        default:
        case GDISP_ROTATE_0:
            x = g->p.x;
            y = g->p.y;
            break;
        case GDISP_ROTATE_90:
            x = g->p.y;
            y = GDISP_SCREEN_HEIGHT - 1 - g->p.x;
            break;
        case GDISP_ROTATE_180:
            x = GDISP_SCREEN_WIDTH - 1 - g->p.x;
            y = GDISP_SCREEN_HEIGHT - 1 - g->p.y;
            break;
        case GDISP_ROTATE_270:
            x = GDISP_SCREEN_HEIGHT - 1 - g->p.y;
            y = g->p.x;
            break;
    }
    return (RAM(g)[xyaddr(x, y)] & xybit(y)) ? White : Black;
}
#    endif

LLDSPEC void gdisp_lld_blit_area(GDisplay *g) {
    uint8_t *buffer     = (uint8_t *)g->p.ptr;
    int      linelength = g->p.cx;
    for (int i = 0; i < g->p.cy; i++) {
        unsigned dstx   = g->p.x;
        unsigned dsty   = g->p.y + i;
        unsigned srcx   = g->p.x1;
        unsigned srcy   = g->p.y1 + i;
        unsigned srcbit = srcy * g->p.x2 + srcx;
        for (int j = 0; j < linelength; j++) {
            uint8_t  src    = buffer[srcbit / 8];
            uint8_t  bit    = 7 - (srcbit % 8);
            uint8_t  bitset = (src >> bit) & 1;
            uint8_t *dst    = &(RAM(g)[xyaddr(dstx, dsty)]);
            if (bitset) {
                *dst |= xybit(dsty);
            } else {
                *dst &= ~xybit(dsty);
            }
            dstx++;
            srcbit++;
        }
    }
    g->flags |= GDISP_FLG_NEEDFLUSH;
}

#    if GDISP_NEED_CONTROL && GDISP_HARDWARE_CONTROL
LLDSPEC void gdisp_lld_control(GDisplay *g) {
    switch (g->p.x) {
        case GDISP_CONTROL_POWER:
            if (g->g.Powermode == (powermode_t)g->p.ptr) return;
            switch ((powermode_t)g->p.ptr) {
                case powerOff:
                case powerSleep:
                case powerDeepSleep:
                    acquire_bus(g);
                    enter_cmd_mode(g);
                    write_cmd(g, ST7565_DISPLAY_OFF);
                    flush_cmd(g);
                    release_bus(g);
                    break;
                case powerOn:
                    acquire_bus(g);
                    enter_cmd_mode(g);
                    write_cmd(g, ST7565_DISPLAY_ON);
                    flush_cmd(g);
                    release_bus(g);
                    break;
                default:
                    return;
            }
            g->g.Powermode = (powermode_t)g->p.ptr;
            return;

        case GDISP_CONTROL_ORIENTATION:
            if (g->g.Orientation == (orientation_t)g->p.ptr) return;
            switch ((orientation_t)g->p.ptr) {
                /* Rotation is handled by the drawing routines */
                case GDISP_ROTATE_0:
                case GDISP_ROTATE_180:
                    g->g.Height = GDISP_SCREEN_HEIGHT;
                    g->g.Width  = GDISP_SCREEN_WIDTH;
                    break;
                case GDISP_ROTATE_90:
                case GDISP_ROTATE_270:
                    g->g.Height = GDISP_SCREEN_WIDTH;
                    g->g.Width  = GDISP_SCREEN_HEIGHT;
                    break;
                default:
                    return;
            }
            g->g.Orientation = (orientation_t)g->p.ptr;
            return;

        case GDISP_CONTROL_CONTRAST:
            g->g.Contrast = (unsigned)g->p.ptr & 63;
            acquire_bus(g);
            enter_cmd_mode(g);
            write_cmd2(g, ST7565_CONTRAST, g->g.Contrast);
            flush_cmd(g);
            release_bus(g);
            return;
    }
}
#    endif  // GDISP_NEED_CONTROL

#endif  // GFX_USE_GDISP
