// Waveshare 13.3" Spectra-6 (E6) panel driver — implements epd.h for the
// ESP32-S3-ePaper-13.3E6 board.
//
// Hardware: 1200 × 1600 pixels, 6 colors, 4 bits-per-pixel packed two
// pixels per byte (full framebuffer = 960 KB). Panel is internally split:
// the left 600 columns are driven via CS_M (master), the right 600 via
// CS_S (slave), both sharing SCK / MOSI / DC / BUSY. Init commands go to
// both halves with both CS lines asserted; framebuffer pushes go to one
// half at a time.
//
// .bin layout (server-rendered, panel-native): row-major, 600 bytes/row
// × 1600 rows. Within each row, bytes [0..300) are the left half and
// [300..600) are the right half. Server writes in this exact order; the
// driver streams the file into a PSRAM buffer then pushes each half's
// columns to its respective CS line.
//
// Init sequence + command set verified against:
//   github.com/teatall/13.3inch_e-Paper_E-Frame (community photo-frame
//   project on the same board) and Waveshare's stock 13in3e demo.

#include "epd.h"
#include "config.h"
#include <LittleFS.h>
#include <qrcode.h>
#include <esp_task_wdt.h>
#include <esp_heap_caps.h>
#include <driver/gpio.h>

static constexpr uint16_t W              = 1200;
static constexpr uint16_t H              = 1600;
static constexpr uint16_t BYTES_PER_ROW  = W / 2;        // 600
static constexpr uint16_t HALF_BYTES_ROW = W / 4;        // 300 per half
static constexpr size_t   FB_BYTES       = (size_t)BYTES_PER_ROW * H;  // 960000

// Driver-level "is the panel awake?" flag. Useful for asserts; not load-bearing.
static bool s_initialized = false;

// PSRAM framebuffer for image render paths. Allocated lazily on first use
// so a fill-only or QR-only path doesn't pay for it. Caller frees via
// release_fb() once the half-pushes are done.
static uint8_t* fb_alloc() {
    return (uint8_t*)heap_caps_malloc(FB_BYTES, MALLOC_CAP_SPIRAM);
}
static void fb_release(uint8_t* fb) { if (fb) heap_caps_free(fb); }

// ── BUSY ───────────────────────────────────────────────────────────────────────
// Active-LOW: panel pulls BUSY low while working, releases high when idle.
// Full refresh on Spectra-6 takes ~25 s; bound the wait at 60 s so a
// hung panel doesn't strand the boot cycle.
static void wait_busy() {
    uint32_t start = millis();
    while (digitalRead(PIN_BUSY) == LOW) {
        if (millis() - start > 60000) return;
        delay(5);
        esp_task_wdt_reset();
    }
}

// ── CS / cmd / data helpers ────────────────────────────────────────────────────
// Pattern: assert one or both CS lines, send cmd byte with DC=LOW, then
// stream data with DC=HIGH, then deassert. Matches Waveshare's reference.

static inline void cs(int pin, int v)        { digitalWrite(pin, v); }
static inline void cs_both(int v)            { cs(PIN_CS_M, v); cs(PIN_CS_S, v); }

static void begin_cmd_both(uint8_t c) {
    cs_both(LOW);
    digitalWrite(PIN_DC, LOW);
    SPI.transfer(c);
    digitalWrite(PIN_DC, HIGH);
}

static void begin_cmd(int cs_pin, uint8_t c) {
    cs(cs_pin, LOW);
    digitalWrite(PIN_DC, LOW);
    SPI.transfer(c);
    digitalWrite(PIN_DC, HIGH);
}

static void send_data_n(const uint8_t* buf, size_t n) {
    SPI.writeBytes(buf, n);
}

static void send_cmd_with_data_both(uint8_t c, const uint8_t* buf, size_t n) {
    begin_cmd_both(c);
    if (buf && n) send_data_n(buf, n);
    cs_both(HIGH);
}

static void send_cmd_with_data_master(uint8_t c, const uint8_t* buf, size_t n) {
    begin_cmd(PIN_CS_M, c);
    if (buf && n) send_data_n(buf, n);
    cs_both(HIGH);
}

// ── Panel reset + init sequence ────────────────────────────────────────────────

static void panel_reset() {
    digitalWrite(PIN_RST, HIGH); delay(30);
    digitalWrite(PIN_RST, LOW);  delay(30);
    digitalWrite(PIN_RST, HIGH); delay(30);
    digitalWrite(PIN_RST, LOW);  delay(30);
    digitalWrite(PIN_RST, HIGH); delay(30);
}

void epd_setup_pins() {
    // Release the PIN_PWR hold latched from the previous deep sleep so we
    // can drive it again. No-op on cold boot (nothing was held). Paired
    // with gpio_hold_en() in epd_sleep() and gpio_deep_sleep_hold_en() in
    // operation.h.
    gpio_hold_dis((gpio_num_t)PIN_PWR);

    pinMode(PIN_PWR,  OUTPUT);
    pinMode(PIN_RST,  OUTPUT);
    pinMode(PIN_DC,   OUTPUT);
    pinMode(PIN_BUSY, INPUT);
    pinMode(PIN_CS_M, OUTPUT);
    pinMode(PIN_CS_S, OUTPUT);

    digitalWrite(PIN_PWR,  HIGH);
    digitalWrite(PIN_CS_M, HIGH);
    digitalWrite(PIN_CS_S, HIGH);
    digitalWrite(PIN_RST,  HIGH);

    // FSPI on S3, manual CS. Matches the 7.3" production speed (4 MHz) —
    // tried 10 MHz first and the panel showed a yellow cast across the
    // body, suggesting bit corruption on long bursts (likely PSRAM-DMA
    // cache coherency or SI margin issues on the long traces inside the
    // all-in-one module).
    SPI.begin(PIN_SCK, -1, PIN_MOSI, -1);
    SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
}

void epd_init() {
    panel_reset();

    static const uint8_t AN_TM_V[]         = {0xC0,0x1C,0x1C,0xCC,0xCC,0xCC,0x15,0x15,0x55};
    static const uint8_t CMD66_V[]         = {0x49,0x55,0x13,0x5D,0x05,0x10};
    static const uint8_t PSR_V[]           = {0xDF,0x69};
    static const uint8_t CDI_V[]           = {0xF7};
    static const uint8_t TCON_V[]          = {0x03,0x03};
    static const uint8_t AGID_V[]          = {0x10};
    static const uint8_t PWS_V[]           = {0x22};
    static const uint8_t CCSET_V[]         = {0x01};
    static const uint8_t TRES_V[]          = {0x04,0xB0,0x03,0x20};
    static const uint8_t PWR_V[]           = {0x0F,0x00,0x28,0x2C,0x28,0x38};
    static const uint8_t EN_BUF_V[]        = {0x07};
    static const uint8_t BTST_P_V[]        = {0xE8,0x28};
    static const uint8_t BOOST_VDDP_EN_V[] = {0x01};
    static const uint8_t BTST_N_V[]        = {0xE8,0x28};
    static const uint8_t BUCK_VDDN_V[]     = {0x01};
    static const uint8_t TFT_VCOM_V[]      = {0x02};

    // AN_TM goes to master only first (per reference; not obvious why but
    // matching exactly de-risks bringup), then a batch of commands to
    // both halves, then a tail of master-only tuning commands.
    send_cmd_with_data_master(0x74, AN_TM_V, sizeof(AN_TM_V));
    send_cmd_with_data_both  (0xF0, CMD66_V, sizeof(CMD66_V));
    send_cmd_with_data_both  (0x00, PSR_V,   sizeof(PSR_V));
    send_cmd_with_data_both  (0x50, CDI_V,   sizeof(CDI_V));
    send_cmd_with_data_both  (0x60, TCON_V,  sizeof(TCON_V));
    send_cmd_with_data_both  (0x86, AGID_V,  sizeof(AGID_V));
    send_cmd_with_data_both  (0xE3, PWS_V,   sizeof(PWS_V));
    send_cmd_with_data_both  (0xE0, CCSET_V, sizeof(CCSET_V));
    send_cmd_with_data_both  (0x61, TRES_V,  sizeof(TRES_V));
    send_cmd_with_data_master(0x01, PWR_V,           sizeof(PWR_V));
    send_cmd_with_data_master(0xB6, EN_BUF_V,        sizeof(EN_BUF_V));
    send_cmd_with_data_master(0x06, BTST_P_V,        sizeof(BTST_P_V));
    send_cmd_with_data_master(0xB7, BOOST_VDDP_EN_V, sizeof(BOOST_VDDP_EN_V));
    send_cmd_with_data_master(0x05, BTST_N_V,        sizeof(BTST_N_V));
    send_cmd_with_data_master(0xB0, BUCK_VDDN_V,     sizeof(BUCK_VDDN_V));
    send_cmd_with_data_master(0xB1, TFT_VCOM_V,      sizeof(TFT_VCOM_V));

    s_initialized = true;
}

// ── Refresh / sleep ────────────────────────────────────────────────────────────

static void epd_refresh() {
    begin_cmd_both(0x04);                        // POWER_ON
    cs_both(HIGH);
    wait_busy();

    delay(50);
    begin_cmd_both(0x12); SPI.transfer(0x00);    // DRF (full refresh)
    cs_both(HIGH);
    wait_busy();

    begin_cmd_both(0x02); SPI.transfer(0x00);    // POWER_OFF
    cs_both(HIGH);
}

void epd_sleep() {
    cs_both(LOW);
    digitalWrite(PIN_DC, LOW);
    SPI.transfer(0x07);                          // DEEP_SLEEP
    digitalWrite(PIN_DC, HIGH);
    SPI.transfer(0xA5);                          // sentinel
    cs_both(HIGH);
    s_initialized = false;

    // Cut the panel power rail. The Waveshare board exposes PIN_PWR
    // specifically for battery operation — the e-ink image persists
    // without VDD (particles are bistable), and dropping the rail kills
    // the boost converter's quiescent draw (~50–500 µA depending on the
    // load on the rail). Latch LOW so it survives deep sleep; paired with
    // gpio_deep_sleep_hold_en() in operation.h just before the chip
    // enters sleep.
    digitalWrite(PIN_PWR, LOW);
    gpio_hold_en((gpio_num_t)PIN_PWR);
}

// ── Draw helpers ───────────────────────────────────────────────────────────────

// DMA-safe scratch buffer (internal SRAM). SPI.writeBytes on ESP32-S3 uses
// GDMA for bulk transfers; DMA can't reliably read from PSRAM-backed
// buffers because the CPU's cache may hold writes that haven't reached
// PSRAM yet — symptom is a yellow/random cast across the panel. Pushing
// from internal SRAM in chunks sidesteps that entirely. 8 KB is a
// per-half-row sweet spot (~27 rows of 300 bytes), enough that the
// per-transfer overhead is negligible.
static constexpr size_t DMA_CHUNK = 8192;
static uint8_t* g_dma_scratch = nullptr;

static void ensure_dma_scratch() {
    if (!g_dma_scratch) {
        g_dma_scratch = (uint8_t*)heap_caps_malloc(
            DMA_CHUNK, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
        Serial.printf("[epd13e6] dma_scratch=%p (free internal=%u)\n",
                      g_dma_scratch,
                      (unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
    }
}

// Push one half's framebuffer slice to its CS line via DMA-safe chunks.
// half_fb points into PSRAM (the full framebuffer); we copy CHUNK bytes
// into internal SRAM, then SPI.writeBytes that. Repeat until done.
static void push_half(int cs_pin, const uint8_t* half_fb, size_t bytes) {
    ensure_dma_scratch();
    if (!g_dma_scratch) return;

    begin_cmd(cs_pin, 0x10);
    for (size_t off = 0; off < bytes; off += DMA_CHUNK) {
        size_t n = (bytes - off < DMA_CHUNK) ? (bytes - off) : DMA_CHUNK;
        memcpy(g_dma_scratch, half_fb + off, n);
        SPI.writeBytes(g_dma_scratch, n);
    }
    cs(cs_pin, HIGH);
}

// Take a full-frame row-major framebuffer (BYTES_PER_ROW × H) and push
// left-then-right halves. Deinterleaves into a PSRAM scratch slice so
// each half is row-contiguous, then push_half streams it through the
// DMA-safe internal-SRAM chunk buffer.
static void push_full_frame(const uint8_t* fb) {
    constexpr size_t HALF_BYTES = (size_t)HALF_BYTES_ROW * H;
    uint8_t* slice = (uint8_t*)heap_caps_malloc(HALF_BYTES, MALLOC_CAP_SPIRAM);
    if (!slice) {
        Serial.printf("[epd13e6] push_full_frame: slice alloc FAILED (free PSRAM=%u)\n",
                      (unsigned)ESP.getFreePsram());
        return;
    }
    Serial.println("[epd13e6] push_full_frame: pushing halves");

    for (uint16_t y = 0; y < H; y++) {
        memcpy(slice + (size_t)y * HALF_BYTES_ROW,
               fb    + (size_t)y * BYTES_PER_ROW,
               HALF_BYTES_ROW);
    }
    push_half(PIN_CS_M, slice, HALF_BYTES);

    for (uint16_t y = 0; y < H; y++) {
        memcpy(slice + (size_t)y * HALF_BYTES_ROW,
               fb    + (size_t)y * BYTES_PER_ROW + HALF_BYTES_ROW,
               HALF_BYTES_ROW);
    }
    push_half(PIN_CS_S, slice, HALF_BYTES);

    heap_caps_free(slice);
    Serial.println("[epd13e6] push_full_frame: done");
}

// ── epd.h surface ──────────────────────────────────────────────────────────────

void epd_fill(uint8_t color) {
    const uint8_t byte = (color << 4) | color;

    // Solid fill needs no framebuffer — stream the byte directly per half.
    for (int half = 0; half < 2; half++) {
        const int p = (half == 0) ? PIN_CS_M : PIN_CS_S;
        begin_cmd(p, 0x10);
        for (size_t i = 0; i < (size_t)HALF_BYTES_ROW * H; i++) {
            SPI.transfer(byte);
        }
        cs(p, HIGH);
    }
    epd_refresh();
}

void epd_draw_image_from_file(fs::File& f) {
    uint8_t* fb = fb_alloc();
    if (!fb) { epd_fill(COLOR_WHITE); return; }

    size_t n = f.read(fb, FB_BYTES);
    if (n != FB_BYTES) {
        fb_release(fb);
        epd_fill(COLOR_WHITE);
        return;
    }

    push_full_frame(fb);
    fb_release(fb);
    epd_refresh();
}

void epd_draw_image_with_border(fs::File& f, uint8_t color, int thickness) {
    if (f.size() != FB_BYTES) {
        epd_fill(color);
        return;
    }
    uint8_t* fb = fb_alloc();
    if (!fb) { epd_fill(color); return; }
    if (f.read(fb, FB_BYTES) != FB_BYTES) { fb_release(fb); epd_fill(color); return; }

    const uint8_t pair = (color << 4) | color;

    // Overlay border in-place. Same x/y orientation as the 7.3" driver:
    // top/bottom solid stripes, plus left/right edges on the middle band.
    for (int y = 0; y < H; y++) {
        uint8_t* row = fb + (size_t)y * BYTES_PER_ROW;
        if (y < thickness || y >= H - thickness) {
            memset(row, pair, BYTES_PER_ROW);
        } else {
            for (int x = 0; x < thickness; x++) {
                if (x & 1) row[x/2] = (row[x/2] & 0xF0) | color;
                else       row[x/2] = (row[x/2] & 0x0F) | (color << 4);
            }
            for (int x = W - thickness; x < W; x++) {
                if (x & 1) row[x/2] = (row[x/2] & 0xF0) | color;
                else       row[x/2] = (row[x/2] & 0x0F) | (color << 4);
            }
        }
    }

    push_full_frame(fb);
    fb_release(fb);
    epd_refresh();
}

void epd_draw_qr(QRCode* qr, uint8_t cellPx, uint8_t bg, uint8_t fg) {
    uint8_t* fb = fb_alloc();
    if (!fb) { epd_fill(bg); return; }

    const uint8_t bg_pair = (bg << 4) | bg;
    memset(fb, bg_pair, FB_BYTES);

    const int qrPx = qr->size * cellPx;
    const int offX = (W - qrPx) / 2;
    const int offY = (H - qrPx) / 2;

    for (int y = offY; y < offY + qrPx; y++) {
        if (y < 0 || y >= H) continue;
        uint8_t* row = fb + (size_t)y * BYTES_PER_ROW;
        const int qy = (y - offY) / cellPx;
        for (int x = offX; x < offX + qrPx; x++) {
            if (x < 0 || x >= W) continue;
            const int qx = (x - offX) / cellPx;
            const uint8_t c = qrcode_getModule(qr, qx, qy) ? fg : bg;
            if (x & 1) row[x/2] = (row[x/2] & 0xF0) | c;
            else       row[x/2] = (row[x/2] & 0x0F) | (c << 4);
        }
    }

    push_full_frame(fb);
    fb_release(fb);
    epd_refresh();
}

// Stream background from LittleFS into the PSRAM framebuffer, overlay the QR
// at (qr_x, qr_y) with the given cell size, then push the full frame in two
// halves. Falls back to a solid fill if the file is missing or wrong size.
static void draw_from_lfs(const char* path, uint8_t fallback_color,
                          QRCode* qr, int qr_x, int qr_y, int qr_cell) {
    File f = LittleFS.open(path, "r");
    if (!f || f.size() != FB_BYTES) {
        Serial.printf("[epd13e6] %s: open=%d size=%u -> fill 0x%X\n",
                      path, (int)(bool)f, f ? (unsigned)f.size() : 0u,
                      fallback_color);
        if (f) f.close();
        epd_fill(fallback_color);
        return;
    }

    uint8_t* fb = fb_alloc();
    if (!fb) {
        Serial.printf("[epd13e6] fb_alloc FAILED (PSRAM free=%u)\n",
                      (unsigned)ESP.getFreePsram());
        f.close();
        epd_fill(fallback_color);
        return;
    }

    if (f.read(fb, FB_BYTES) != FB_BYTES) {
        Serial.println("[epd13e6] read short");
        fb_release(fb); f.close(); epd_fill(fallback_color); return;
    }
    f.close();

    // Sample first 8 bytes of each quarter of the framebuffer so we can
    // verify what we're about to push matches the on-disk .bin. If the
    // panel shows wrong colors with these sample bytes looking right,
    // corruption is downstream of the CPU.
    Serial.printf("[epd13e6] fb sample: head=%02x%02x%02x%02x%02x%02x%02x%02x ",
                  fb[0], fb[1], fb[2], fb[3], fb[4], fb[5], fb[6], fb[7]);
    size_t q = FB_BYTES / 4;
    Serial.printf("q1=%02x%02x%02x%02x q2=%02x%02x%02x%02x q3=%02x%02x%02x%02x\n",
                  fb[q+0], fb[q+1], fb[q+2], fb[q+3],
                  fb[2*q+0], fb[2*q+1], fb[2*q+2], fb[2*q+3],
                  fb[3*q+0], fb[3*q+1], fb[3*q+2], fb[3*q+3]);

    const int qr_px = qr->size * qr_cell;
    const int y0 = max(qr_y, 0);
    const int y1 = min(qr_y + qr_px, (int)H);
    const int x0 = max(qr_x, 0);
    const int x1 = min(qr_x + qr_px, (int)W);
    for (int y = y0; y < y1; y++) {
        uint8_t* row = fb + (size_t)y * BYTES_PER_ROW;
        const int qy = (y - qr_y) / qr_cell;
        for (int x = x0; x < x1; x++) {
            const int qx = (x - qr_x) / qr_cell;
            const uint8_t c = qrcode_getModule(qr, qx, qy) ? COLOR_BLACK : COLOR_WHITE;
            if (x & 1) row[x/2] = (row[x/2] & 0xF0) | c;
            else       row[x/2] = (row[x/2] & 0x0F) | (c << 4);
        }
    }

    push_full_frame(fb);
    fb_release(fb);
    epd_refresh();
}

// QR overlay coordinates for the 13.3" portrait setup screens. Must stay
// in sync with scripts/gen_screens_13e6.py — the bg .bin leaves a white
// rectangle exactly the size of QR_MODS × QR_CELL at (X, Y), and the
// firmware paints the live QR into it. Mismatch = the QR draws over
// decorative borders or the QR placeholder shows through.
void epd_draw_ap_screen(QRCode* qr) {
    draw_from_lfs("/ap_bg.bin",       COLOR_YELLOW, qr, 642, 590, 14);
}

void epd_draw_ap_screen_retry(QRCode* qr) {
    draw_from_lfs("/ap_bg_retry.bin", COLOR_RED,    qr, 642, 590, 14);
}

void epd_draw_setup_screen(QRCode* qr) {
    draw_from_lfs("/setup_bg.bin",    COLOR_GREEN,  qr, 313, 750, 14);
}
