#include "ssd1619.h"

#include <stdbool.h>
#include <string.h>

#include "asmUtil.h"
#include "barcode.h"
#include "board.h"
#include "cpu.h"
#include "font.h"
#include "lut.h"
#include "printf.h"
#include "screen.h"
// #include "settings.h"
#include "sleep.h"
#include "spi.h"
#include "timer.h"
#include "wdt.h"

#define CMD_DRV_OUTPUT_CTRL 0x01
#define CMD_SOFT_START_CTRL 0x0C
#define CMD_ENTER_SLEEP 0x10
#define CMD_DATA_ENTRY_MODE 0x11
#define CMD_SOFT_RESET 0x12
#define CMD_SOFT_RESET2 0x13
#define CMD_SETUP_VOLT_DETECT 0x15
#define CMD_TEMP_SENSOR_CONTROL 0x18
#define CMD_ACTIVATION 0x20
#define CMD_DISP_UPDATE_CTRL 0x21
#define CMD_DISP_UPDATE_CTRL2 0x22
#define CMD_WRITE_FB_BW 0x24
#define CMD_WRITE_FB_RED 0x26
#define CMD_UNKNOWN_1 0x2B
#define CMD_LOAD_OTP_LUT 0x31
#define CMD_WRITE_LUT 0x32
#define CMD_BORDER_WAVEFORM_CTRL 0x3C
#define CMD_WINDOW_X_SIZE 0x44
#define CMD_WINDOW_Y_SIZE 0x45
#define CMD_WRITE_PATTERN_RED 0x46
#define CMD_WRITE_PATTERN_BW 0x47
#define CMD_XSTART_POS 0x4E
#define CMD_YSTART_POS 0x4F
#define CMD_ANALOG_BLK_CTRL 0x74
#define CMD_DIGITAL_BLK_CTRL 0x7E

#define SCREEN_CMD_CLOCK_ON 0x80
#define SCREEN_CMD_CLOCK_OFF 0x01
#define SCREEN_CMD_ANALOG_ON 0x40
#define SCREEN_CMD_ANALOG_OFF 0x02
#define SCREEN_CMD_LATCH_TEMPERATURE_VAL 0x20
#define SCREEN_CMD_LOAD_LUT 0x10
#define SCREEN_CMD_USE_MODE_2 0x08  // modified commands 0x10 and 0x04
#define SCREEN_CMD_REFRESH 0xC7

#define commandEnd() \
    do {             \
        P1_7 = 1;    \
    } while (0)

#define markCommand() \
    do {              \
        P2_2 = 0;     \
    } while (0)

#define markData() \
    do {           \
        P2_2 = 1;  \
    } while (0)

extern void dump(uint8_t* __xdata a, uint16_t __xdata l);  // remove me when done

static uint8_t __xdata epdCharSize = 1;   // character size, 1 or 2 (doubled)
static bool __xdata directionY = true;    // print direction, X or Y (true)
static uint8_t __xdata rbuffer[32];       // used to rotate bits around
static uint16_t __xdata fontCurXpos = 0;  // current X value we're working with
static uint16_t __xdata fontCurYpos = 0;  // current Y value we're working with
static uint8_t __xdata currentLut = 0;
uint8_t __xdata dispLutSize = 0;  // we'll need to expose this in the 'capabilities' flag

static bool __xdata isInited = false;

bool __xdata epdGPIOActive = false;

#define LUT_BUFFER_SIZE 128
static uint8_t waveformbuffer[LUT_BUFFER_SIZE];
uint8_t __xdata customLUT[LUT_BUFFER_SIZE] =  {0};

struct waveform10* __xdata waveform10 = (struct waveform10*)waveformbuffer;  // holds the LUT/waveform
struct waveform* __xdata waveform7 = (struct waveform*)waveformbuffer;       // holds the LUT/waveform

#pragma callee_saves epdBusySleep
#pragma callee_saves epdBusyWait
static void epdBusySleep(uint32_t timeout) {
    uint8_t tmp_P2FUNC = P2FUNC;
    uint8_t tmp_P2DIR = P2DIR;
    uint8_t tmp_P2PULL = P2PULL;
    uint8_t tmp_P2LVLSEL = P2LVLSEL;
    P2FUNC &= 0xfd;
    P2DIR |= 2;
    P2PULL |= 2;
    P2LVLSEL |= 2;

    P2CHSTA &= 0xfd;
    P2INTEN |= 2;
    P2CHSTA &= 0xfd;
    sleepForMsec(timeout);
    wdtOn();
    P2CHSTA &= 0xfd;
    P2INTEN &= 0xfd;

    P2FUNC = tmp_P2FUNC;
    P2DIR = tmp_P2DIR;
    P2PULL = tmp_P2PULL;
    P2LVLSEL = tmp_P2LVLSEL;
    eepromPrvDeselect();
}
static void epdBusyWait(uint32_t timeout) {
    uint32_t __xdata start = timerGet();

    while (timerGet() - start < timeout) {
        if (!P2_1)
            return;
    }
    pr("screen timeout %lu ticks :(\n", timerGet() - start);
    while (1)
        ;
}
static void commandReadBegin(uint8_t cmd) {
    epdSelect();
    markCommand();
    spiByte(cmd);  // dump LUT

    P0DIR = (P0DIR & ~(1 << 0)) | (1 << 1);
    P0 &= ~(1 << 0);
    P0FUNC &= ~((1 << 0) | (1 << 1));
    P2_2 = 1;
}
static void commandReadEnd() {
    // set up pins for spi (0.0,0.1,0.2)
    P0FUNC |= (1 << 0) | (1 << 1);
    epdDeselect();
}
#pragma callee_saves epdReadByte
static uint8_t epdReadByte() {
    uint8_t val = 0, i;

    for (i = 0; i < 8; i++) {
        P0_0 = 1;
        __asm__("nop\nnop\nnop\nnop\nnop\nnop\n");
        val <<= 1;
        if (P0_1)
            val++;
        P0_0 = 0;
        __asm__("nop\nnop\nnop\nnop\nnop\nnop\n");
    }

    return val;
}
static void shortCommand(uint8_t cmd) {
    epdSelect();
    markCommand();
    spiTXByte(cmd);
    epdDeselect();
}
static void shortCommand1(uint8_t cmd, uint8_t arg) {
    epdSelect();
    markCommand();
    spiTXByte(cmd);
    markData();
    spiTXByte(arg);
    epdDeselect();
}
static void shortCommand2(uint8_t cmd, uint8_t arg1, uint8_t arg2) {
    epdSelect();
    markCommand();
    spiTXByte(cmd);
    markData();
    spiTXByte(arg1);
    spiTXByte(arg2);
    epdDeselect();
}
static void commandBegin(uint8_t cmd) {
    epdSelect();
    markCommand();
    spiTXByte(cmd);
    markData();
}
static void epdReset() {
    timerDelay(TIMER_TICKS_PER_SECOND / 1000);
    P2_0 = 0;
    timerDelay(TIMER_TICKS_PER_SECOND / 1000);
    P2_0 = 1;
    timerDelay(TIMER_TICKS_PER_SECOND / 1000);

    shortCommand(CMD_SOFT_RESET);  // software reset
    timerDelay(TIMER_TICKS_PER_SECOND / 1000);
    shortCommand(CMD_SOFT_RESET2);
    timerDelay(TIMER_TICKS_PER_SECOND / 1000);
}
void epdConfigGPIO(bool setup) {
    // data / _command: 2.2
    // busy             2.1
    // reset            2.0
    // _select          1.7
    // bs1              1.2

    // GENERIC SPI BUS PINS
    // spi.clk          0.0
    // spi.mosi         0.1
    if (epdGPIOActive == setup) return;
    if (setup) {
        P2DIR |= (1 << 1);                // busy as input
        P2DIR &= ~((1 << 2) | (1 << 0));  // D/C and Reset as output
        P1DIR &= ~((1 << 7) | (1 << 2));  // select and bs1 as output
        P1_2 = 0;                         // select 4-wire SPI / BS1 = low
        P1_7 = 1;                         // deselect EPD
    } else {
        P2DIR |= ((1 << 2) | (1 << 0));  // DC and Reset as input
        P2 &= ~((1 << 2) | (1 << 0));
        P1DIR |= ((1 << 7) | (1 << 2));  // Select and BS1 as input
        P2 &= ~((1 << 7));
    }
    epdGPIOActive = setup;
}
void epdEnterSleep() {
    P2_0 = 0;
    timerDelay(10);
    P2_0 = 1;
    timerDelay(50);
    shortCommand(CMD_SOFT_RESET2);
    epdBusyWait(TIMER_TICKS_PER_MS * 15);
    shortCommand1(CMD_ENTER_SLEEP, 0x03);
    isInited = false;
}
void epdSetup() {
    epdReset();
    shortCommand1(CMD_ANALOG_BLK_CTRL, 0x54);
    shortCommand1(CMD_DIGITAL_BLK_CTRL, 0x3B);
    shortCommand2(CMD_UNKNOWN_1, 0x04, 0x63);

    commandBegin(CMD_SOFT_START_CTRL);
    epdSend(0x8f);
    epdSend(0x8f);
    epdSend(0x8f);
    epdSend(0x3f);
    commandEnd();

    commandBegin(CMD_DRV_OUTPUT_CTRL);
    epdSend((SCREEN_HEIGHT - 1) & 0xff);
    epdSend((SCREEN_HEIGHT - 1) >> 8);
    epdSend(0x00);
    commandEnd();

    // shortCommand1(CMD_DATA_ENTRY_MODE, 0x03);
    // shortCommand1(CMD_BORDER_WAVEFORM_CTRL, 0xC0); // blurry edges
    shortCommand1(CMD_BORDER_WAVEFORM_CTRL, 0x01);
    shortCommand1(CMD_TEMP_SENSOR_CONTROL, 0x80);
    shortCommand1(CMD_DISP_UPDATE_CTRL2, 0xB1);  // mode 1 (i2C)
    // shortCommand1(CMD_DISP_UPDATE_CTRL2, 0xB9);  // mode 2?
    shortCommand(CMD_ACTIVATION);
    epdBusyWait(TIMER_TICKS_PER_SECOND);
    isInited = true;
    currentLut = EPD_LUT_DEFAULT;
}
static uint8_t epdGetStatus() {
    uint8_t sta;
    commandReadBegin(0x2F);
    sta = epdReadByte();
    commandReadEnd();
    return sta;
}
uint16_t epdGetBattery(void) {
    uint16_t voltage = 2600;
    uint8_t val;

    timerDelay(50);
    P2_0 = 0;
    timerDelay(50);
    P2_0 = 1;
    timerDelay(50);

    shortCommand(CMD_SOFT_RESET);  // software reset
    epdBusyWait(TIMER_TICKS_PER_MS * 30);
    shortCommand(CMD_SOFT_RESET2);
    epdBusyWait(TIMER_TICKS_PER_MS * 30);

    shortCommand1(CMD_DISP_UPDATE_CTRL2, SCREEN_CMD_CLOCK_ON | SCREEN_CMD_ANALOG_ON);
    shortCommand(CMD_ACTIVATION);
    epdBusyWait(TIMER_TICKS_PER_MS * 100);

    for (val = 3; val < 8; val++) {
        shortCommand1(CMD_SETUP_VOLT_DETECT, val);
        epdBusyWait(TIMER_TICKS_PER_MS * 100);
        if (epdGetStatus() & 0x10) {  // set if voltage is less than threshold ( == 1.9 + val / 10)
            voltage = 1850 + mathPrvMul8x8(val, 100);
            break;
        }
    }

    shortCommand(CMD_SOFT_RESET2);
    epdBusyWait(TIMER_TICKS_PER_MS * 15);
    shortCommand1(CMD_ENTER_SLEEP, 0x03);

    return voltage;
}

void loadFixedTempOTPLUT() {
    shortCommand1(0x18, 0x48);                   // external temp sensor
    shortCommand2(0x1A, 0x05, 0x00);             // < temp register
    shortCommand1(CMD_DISP_UPDATE_CTRL2, 0xB1);  // mode 1 (i2C)
    shortCommand(CMD_ACTIVATION);
    epdBusyWait(TIMER_TICKS_PER_SECOND);
}
static void writeLut() {
    commandBegin(CMD_WRITE_LUT);
    for (uint8_t i = 0; i < (dispLutSize * 10); i++)
        epdSend(waveformbuffer[i]);
    commandEnd();
}
static void readLut() {
    commandReadBegin(0x33);
    uint16_t checksum = 0;
    uint16_t ident = 0;
    uint16_t shortl = 0;
    for (uint16_t c = 0; c < LUT_BUFFER_SIZE; c++) {
        waveformbuffer[c] = epdReadByte();
    }
    commandReadEnd();
}
static uint8_t getLutSize() {
    uint8_t ref = 0;
    for (uint8_t c = (LUT_BUFFER_SIZE - 4); c > 16; c--) {
        uint8_t check = waveformbuffer[c];
        for (uint8_t d = 1; d < 4; d++) {
            if (waveformbuffer[c + d] != check) {
                ref = c;
                goto end;
            }
        }
    }
end:;
    return ref + 1;
}
static void lutGroupDisable(uint8_t group) {
    if (dispLutSize == 7) {
        memset(&(waveform7->group[group]), 0x00, 5);
    } else {
        memset(&(waveform10->group[group]), 0x00, 5);
    }
}
static void lutGroupSpeedup(uint8_t group, uint8_t speed) {
    if (dispLutSize == 7) {
        for (uint8_t i = 0; i < 4; i++) {
            waveform7->group[group].phaselength[i] = 1 + (waveform7->group[group].phaselength[i] / speed);
        }
    } else {
        for (uint8_t i = 0; i < 4; i++) {
            waveform10->group[group].phaselength[i] = 1 + (waveform10->group[group].phaselength[i] / speed);
        }
    }
}
static void lutGroupRepeat(uint8_t group, uint8_t repeat) {
    if (dispLutSize == 7) {
        waveform7->group[group].repeat = repeat;
    } else {
        waveform10->group[group].repeat = repeat;
    }
}
static void lutGroupRepeatReduce(uint8_t group, uint8_t factor) {
    if (dispLutSize == 7) {
        waveform7->group[group].repeat = waveform7->group[group].repeat / factor;
    } else {
        waveform10->group[group].repeat = waveform10->group[group].repeat / factor;
    }
}
void selectLUT(uint8_t lut) {
    if (currentLut == lut) {
        return;
    }

    // Handling if we received an OTA LUT
    if (lut == EPD_LUT_OTA) {
        memcpy(waveformbuffer, customLUT, dispLutSize * 10);
        writeLut();
        currentLut = lut;
        return;
    }

    if (currentLut != EPD_LUT_DEFAULT) {
        // load the 'default' LUT for the current temperature in the EPD lut register
        shortCommand1(CMD_DISP_UPDATE_CTRL2, 0xB1);  // mode 1?
        shortCommand(CMD_ACTIVATION);
        epdBusyWait(TIMER_TICKS_PER_SECOND);
    }

    currentLut = lut;

    // if we're going to be using the default LUT, we're done here.
    if (lut == EPD_LUT_DEFAULT) {
        return;
    }

    // download the current LUT from the waveform buffer
    readLut();

    if (dispLutSize == 0) {
        dispLutSize = getLutSize();
        dispLutSize /= 10;
        pr("lut size = %d\n", dispLutSize);
#ifdef PRINT_LUT
        dump(waveformbuffer, LUT_BUFFER_SIZE);
#endif
        memcpy(customLUT, waveformbuffer, dispLutSize * 10);
    }

    switch (lut) {
        default:
        case EPD_LUT_NO_REPEATS:
            lutGroupDisable(LUTGROUP_NEGATIVE);
            lutGroupDisable(LUTGROUP_FASTBLINK);
            lutGroupRepeat(LUTGROUP_SLOWBLINK, 0);
            lutGroupSpeedup(LUTGROUP_SET, 2);
            lutGroupSpeedup(LUTGROUP_IMPROVE_SHARPNESS, 2);
            lutGroupRepeatReduce(LUTGROUP_IMPROVE_SHARPNESS, 2);
            lutGroupSpeedup(LUTGROUP_IMPROVE_REDS, 2);
            lutGroupRepeatReduce(LUTGROUP_IMPROVE_REDS, 2);
            lutGroupDisable(LUTGROUP_UNUSED);
            break;
        case EPD_LUT_FAST_NO_REDS:
            lutGroupDisable(LUTGROUP_NEGATIVE);
            lutGroupDisable(LUTGROUP_FASTBLINK);
            lutGroupDisable(LUTGROUP_SLOWBLINK);
            lutGroupSpeedup(LUTGROUP_SET, 2);
            lutGroupDisable(LUTGROUP_IMPROVE_REDS);
            lutGroupDisable(LUTGROUP_IMPROVE_SHARPNESS);
            lutGroupDisable(LUTGROUP_UNUSED);
            break;
        case EPD_LUT_FAST:
            lutGroupDisable(LUTGROUP_NEGATIVE);
            lutGroupDisable(LUTGROUP_FASTBLINK);
            lutGroupDisable(LUTGROUP_SLOWBLINK);
            lutGroupRepeat(LUTGROUP_SET, 1);
            lutGroupSpeedup(LUTGROUP_SET, 2);
            lutGroupDisable(LUTGROUP_IMPROVE_SHARPNESS);
            lutGroupDisable(LUTGROUP_IMPROVE_REDS);
            lutGroupDisable(LUTGROUP_UNUSED);
            break;
    }

    if (dispLutSize == 10) {
        lutGroupDisable(LUTGROUP_UNUSED);
        lutGroupDisable(LUTGROUP_UNKNOWN);
        lutGroupDisable(LUTGROUP_UNUSED3);
        lutGroupDisable(LUTGROUP_UNUSED4);
    }
    writeLut();
}

void setWindowX(uint16_t start, uint16_t end) {
    shortCommand2(CMD_WINDOW_X_SIZE, start / 8, end / 8 - 1);
}
void setWindowY(uint16_t start, uint16_t end) {
    commandBegin(CMD_WINDOW_Y_SIZE);
    epdSend((start)&0xff);
    epdSend((start) >> 8);
    epdSend((end - 1) & 0xff);
    epdSend((end - 1) >> 8);
    commandEnd();
}
void setPosXY(uint16_t x, uint16_t y) {
    shortCommand1(CMD_XSTART_POS, (uint8_t)(x / 8));
    commandBegin(CMD_YSTART_POS);
    epdSend((y)&0xff);
    epdSend((y) >> 8);
    commandEnd();
}
void setColorMode(uint8_t red, uint8_t bw) {
    shortCommand1(CMD_DISP_UPDATE_CTRL, (red << 4) | bw);
}
void fillWindowWithPattern(bool color) {
    if (color == EPD_COLOR_RED) {
        shortCommand1(CMD_WRITE_PATTERN_RED, 0x00);
    } else {
        shortCommand1(CMD_WRITE_PATTERN_BW, 0x00);
    }
}
void clearWindow(bool color) {
    if (color == EPD_COLOR_RED) {
        shortCommand1(CMD_WRITE_PATTERN_RED, 0x66);
    } else {
        shortCommand1(CMD_WRITE_PATTERN_BW, 0x66);
    }
}
void clearScreen() {
    setWindowX(0, SCREEN_WIDTH);
    setWindowY(0, SCREEN_HEIGHT);
    setPosXY(0, 0);
    shortCommand1(CMD_DATA_ENTRY_MODE, 3);  // was 3
    shortCommand1(CMD_WRITE_PATTERN_BW, 0x66);
    epdBusyWait(TIMER_TICKS_PER_MS * 100);
    shortCommand1(CMD_WRITE_PATTERN_RED, 0x66);
    epdBusyWait(TIMER_TICKS_PER_MS * 100);
}
void draw() {
    shortCommand1(0x22, 0xCF);
    // shortCommand1(0x22, SCREEN_CMD_REFRESH);
    shortCommand(0x20);
    epdBusyWait(TIMER_TICKS_PER_SECOND * 120);
}
void drawNoWait() {
    shortCommand1(0x22, 0xCF);
    // shortCommand1(0x22, SCREEN_CMD_REFRESH);
    shortCommand(0x20);
}
void drawWithSleep() {
    shortCommand1(0x22, 0xCF);
    // shortCommand1(0x22, SCREEN_CMD_REFRESH);
    shortCommand(0x20);
    uint8_t tmp_P2FUNC = P2FUNC;
    uint8_t tmp_P2DIR = P2DIR;
    uint8_t tmp_P2PULL = P2PULL;
    uint8_t tmp_P2LVLSEL = P2LVLSEL;
    P2FUNC &= 0xfd;
    P2DIR |= 2;
    P2PULL |= 2;
    P2LVLSEL |= 2;

    P2CHSTA &= 0xfd;
    P2INTEN |= 2;
    P2CHSTA &= 0xfd;
    sleepForMsec(TIMER_TICKS_PER_SECOND * 120);
    wdtOn();
    P2CHSTA &= 0xfd;
    P2INTEN &= 0xfd;

    P2FUNC = tmp_P2FUNC;
    P2DIR = tmp_P2DIR;
    P2PULL = tmp_P2PULL;
    P2LVLSEL = tmp_P2LVLSEL;
    eepromPrvDeselect();
}
void epdWaitRdy() {
    epdBusyWait(TIMER_TICKS_PER_SECOND * 120);
}
void drawLineHorizontal(bool color, uint16_t x1, uint16_t x2, uint16_t y) {
    setWindowX(x1, x2);
    setWindowY(y, y + 1);
    if (color) {
        shortCommand1(CMD_WRITE_PATTERN_RED, 0xE6);
    } else {
        shortCommand1(CMD_WRITE_PATTERN_BW, 0xE6);
    }
    epdBusyWait(TIMER_TICKS_PER_MS * 100);
}
void drawLineVertical(bool color, uint16_t x, uint16_t y1, uint16_t y2) {
    setWindowY(y1, y2);
    setWindowX(x, x + 8);
    shortCommand1(CMD_DATA_ENTRY_MODE, 3);
    setPosXY(x, y1);
    if (color) {
        commandBegin(CMD_WRITE_FB_RED);
    } else {
        commandBegin(CMD_WRITE_FB_BW);
    }
    uint8_t __xdata c = 0x80;
    c >>= (x % 8);
    for (; y1 < y2; y1++) {
        epdSend(c);
    }
    commandEnd();
}
void beginFullscreenImage() {
    setColorMode(EPD_MODE_NORMAL, EPD_MODE_INVERT);
    setWindowX(0, SCREEN_WIDTH);
    setWindowY(0, SCREEN_HEIGHT);
    shortCommand1(CMD_DATA_ENTRY_MODE, 3);
    setPosXY(0, 0);
}
void beginWriteFramebuffer(bool color) {
    if (color == EPD_COLOR_RED) {
        commandBegin(CMD_WRITE_FB_RED);
    } else {
        commandBegin(CMD_WRITE_FB_BW);
    }
    epdDeselect();
}
void endWriteFramebuffer() {
    commandEnd();
}
void loadRawBitmap(uint8_t* bmp, uint16_t x, uint16_t y, bool color) {
    uint16_t xsize = bmp[0] / 8;
    if (bmp[0] % 8) xsize++;
    uint16_t size = xsize * bmp[1];
    setWindowX(x, x + (xsize * 8));
    setWindowY(y, bmp[1] + y);
    setPosXY(x, y);
    shortCommand1(CMD_DATA_ENTRY_MODE, 3);
    if (color) {
        commandBegin(CMD_WRITE_FB_RED);
    } else {
        commandBegin(CMD_WRITE_FB_BW);
    }
    bmp += 2;
    while (size--) {
        epdSend(*(bmp++));
    }
    commandEnd();
}
void printBarcode(const uint8_t* string, uint16_t x, uint16_t y) {
    setWindowY(y, 1);
    setWindowX(x, x + 8);
    setPosXY(x, y);
    shortCommand1(CMD_DATA_ENTRY_MODE, 1);
    commandBegin(CMD_WRITE_FB_BW);
    struct BarcodeInfo __xdata bci = {
        .str = string,
    };
    while (!barcodeIsDone(&bci)) {
        if (barcodeNextBar(&bci)) {
            epdSend(0xFF);
        } else {
            epdSend(0x00);
        }
    }
    commandEnd();
}
// stuff for printing text
static void pushXFontBytesToEPD(uint8_t byte1, uint8_t byte2) {
    if (epdCharSize == 1) {
        uint8_t offset = 7 - (fontCurXpos % 8);
        for (uint8_t c = 0; c < 8; c++) {
            if (byte2 & (1 << (7 - c))) rbuffer[c] |= (1 << offset);
        }
        for (uint8_t c = 0; c < 8; c++) {
            if (byte1 & (1 << (7 - c))) rbuffer[8 + c] |= (1 << offset);
        }
        fontCurXpos++;
    } else {
        uint8_t offset = 6 - (fontCurXpos % 8);
        // double font size
        for (uint8_t c = 0; c < 8; c++) {
            if (byte2 & (1 << (7 - c))) {
                rbuffer[c * 2] |= (3 << offset);
                rbuffer[(c * 2) + 1] |= (3 << offset);
            }
        }
        for (uint8_t c = 0; c < 8; c++) {
            if (byte1 & (1 << (7 - c))) {
                rbuffer[(c * 2) + 16] |= (3 << offset);
                rbuffer[(c * 2) + 17] |= (3 << offset);
            }
        }
        fontCurXpos += 2;
    }
    if (fontCurXpos % 8 == 0) {
        // next byte, flush current byte to EPD
        for (uint8_t i = 0; i < (16 * epdCharSize); i++) {
            epdSend(rbuffer[i]);
        }
        memset(rbuffer, 0, 32);
    }
}
static void bufferByteShift(uint8_t byte) {
    /*
                rbuffer[0] = 0;   // previous value
                rbuffer[1] = y%8; // offset
                rbuffer[2] = 0;   // current byte counter;
                rbuffer[3] = 1+(epdCharsize*2);
    */

    if (rbuffer[1] == 0) {
        epdSend(byte);
    } else {
        uint8_t offset = rbuffer[1];
        rbuffer[0] |= (byte >> offset);
        epdSend(rbuffer[0]);
        // epdSend(byte);
        rbuffer[0] = (byte << (8 - offset));
        rbuffer[2]++;
        if (rbuffer[2] == rbuffer[3]) {
            epdSend(rbuffer[0]);
            rbuffer[0] = 0;
            rbuffer[2] = 0;
        }
    }
}
static void pushYFontBytesToEPD(uint8_t byte1, uint8_t byte2) {
    if (epdCharSize == 2) {
        for (uint8_t j = 0; j < 2; j++) {
            uint8_t c = 0;
            for (uint8_t i = 7; i != 255; i--) {
                if (byte1 & (1 << i)) c |= (0x03 << ((i % 4) * 2));
                if ((i % 4) == 0) {
                    bufferByteShift(c);
                    c = 0;
                }
            }
            for (uint8_t i = 7; i != 255; i--) {
                if (byte2 & (1 << i)) c |= (0x03 << ((i % 4) * 2));
                if ((i % 4) == 0) {
                    bufferByteShift(c);
                    c = 0;
                }
            }
        }
    } else {
        bufferByteShift(byte1);
        bufferByteShift(byte2);
    }
}
void writeCharEPD(uint8_t c) {
    // Writes a single character to the framebuffer
    bool empty = true;
    for (uint8_t i = 0; i < 20; i++) {
        if (font[c][i]) empty = false;
    }
    if (empty) {
        for (uint8_t i = 0; i < 8; i++) {
            if (directionY) {
                pushYFontBytesToEPD(0x00, 0x00);
            } else {
                pushXFontBytesToEPD(0x00, 0x00);
            }
        }
        return;
    }

    uint8_t begin = 0;
    while (font[c][begin] == 0x00 && font[c][begin + 1] == 0x00) {
        begin += 2;
    }

    uint8_t end = 20;
    while (font[c][end - 1] == 0x00 && font[c][end - 2] == 0x00) {
        end -= 2;
    }

    for (uint8_t pos = begin; pos < end; pos += 2) {
        if (directionY) {
            pushYFontBytesToEPD(font[c][pos + 1], font[c][pos]);
        } else {
            pushXFontBytesToEPD(font[c][pos], font[c][pos + 1]);
        }
    }

    // spacing between characters
    if (directionY) {
        pushYFontBytesToEPD(0x00, 0x00);
    } else {
        pushXFontBytesToEPD(0x00, 0x00);
    }
}

// Print text to the EPD. Origin is top-left
void epdPrintBegin(uint16_t x, uint16_t y, bool direction, bool fontsize, bool color) {
    directionY = direction;
    epdCharSize = 1 + fontsize;
    if (directionY) {
        uint8_t extra = 0;

        // provisions for dealing with font in Y direction, byte-unaligned
        if (x % 8) {
            extra = 8;
            rbuffer[0] = 0;      // previous value
            rbuffer[1] = x % 8;  // offset
            rbuffer[2] = 0;      // current byte counter;
            rbuffer[3] = (epdCharSize * 2);
        } else {
            rbuffer[1] = 0;
        }

        setWindowY(y, 1);
        if (epdCharSize == 2) {
            setWindowX(x, x + 32 + extra);
            setPosXY(x, y);
        } else {
            setWindowX(x, x + 16 + extra);
            setPosXY(x, y);
        }
        shortCommand1(CMD_DATA_ENTRY_MODE, 1);  // was 3
    } else {
        if (epdCharSize == 2) {
            x /= 2;
            x *= 2;
            setWindowY(y, y + 32);
        } else {
            setWindowY(y, y + 16);
        }
        setPosXY(x, y);
        fontCurXpos = x;
        setWindowX(x, SCREEN_WIDTH);
        shortCommand1(CMD_DATA_ENTRY_MODE, 7);
        memset(rbuffer, 0, 32);
    }

    if (color) {
        commandBegin(CMD_WRITE_FB_RED);
    } else {
        commandBegin(CMD_WRITE_FB_BW);
    }
}
void epdPrintEnd() {
    if (!directionY && ((fontCurXpos % 8) != 0)) {
        for (uint8_t i = 0; i < (16 * epdCharSize); i++) {
            epdSend(rbuffer[i]);
        }
    }
    commandEnd();
}

extern uint8_t __xdata blockXferBuffer[];

void readRam() {
    setWindowY(296, 0);
    setWindowX(0, 8);
    setPosXY(0, 296);
    shortCommand1(CMD_DATA_ENTRY_MODE, 1);  // was 3
    shortCommand1(0x41, 0x00);
    commandReadBegin(0x27);
    epdReadByte();

    for (uint16_t c = 0; c < 293; c++) {
        blockXferBuffer[c] = epdReadByte() | 0x10;
    }
    commandReadEnd();
    commandBegin(CMD_WRITE_FB_BW);
    for (uint16_t c = 0; c < 296; c++) {
        epdSend(blockXferBuffer[c]);
    }
    commandEnd();
}
