#include "EInkParallelDisplay.h"

#ifdef USE_EINK_PARALLELDISPLAY

#include "Wire.h"
#include "variant.h"
#include <Arduino.h>
#include <atomic>
#include <stdlib.h>
#include <string.h>

#include "FastEPD.h"

// Thresholds for choosing partial vs full update
#ifndef EPD_PARTIAL_THRESHOLD_ROWS
#define EPD_PARTIAL_THRESHOLD_ROWS 128 // if changed region <= this many rows, prefer partial
#endif
#ifndef EPD_FULLSLOW_PERIOD
#define EPD_FULLSLOW_PERIOD 100 // every N full updates do a slow (CLEAR_SLOW) full refresh
#endif
#ifndef EPD_RESPONSIVE_MIN_MS
#define EPD_RESPONSIVE_MIN_MS 1000 // simple rate-limit (ms) for responsive updates
#endif

EInkParallelDisplay::EInkParallelDisplay(uint16_t width, uint16_t height, EpdRotation rot) : epaper(nullptr), rotation(rot)
{
    LOG_INFO("init EInkParallelDisplay");
    // Set dimensions in OLEDDisplay base class
    this->geometry = GEOMETRY_RAWMODE;
    this->displayWidth = width;
    this->displayHeight = height;

    // Round shortest side up to nearest byte, to prevent truncation causing an undersized buffer
    uint16_t shortSide = min(width, height);
    uint16_t longSide = max(width, height);
    if (shortSide % 8 != 0)
        shortSide = (shortSide | 7) + 1;

    this->displayBufferSize = longSide * (shortSide / 8);

#ifdef EINK_LIMIT_GHOSTING_PX
    // allocate dirty pixel buffer same size as epaper buffers (rowBytes * height)
    size_t rowBytes = (this->displayWidth + 7) / 8;
    dirtyPixelsSize = rowBytes * this->displayHeight;
    dirtyPixels = (uint8_t *)calloc(dirtyPixelsSize, 1);
    ghostPixelCount = 0;
#endif
}

EInkParallelDisplay::~EInkParallelDisplay()
{
#ifdef EINK_LIMIT_GHOSTING_PX
    if (dirtyPixels) {
        free(dirtyPixels);
        dirtyPixels = nullptr;
    }
#endif
    // If an async full update is running, wait for it to finish
    if (asyncFullRunning.load()) {
        // wait a short while for task to finish
        for (int i = 0; i < 50 && asyncFullRunning.load(); ++i) {
            delay(50);
        }
        if (asyncTaskHandle) {
            // Let it finish or delete it
            vTaskDelete(asyncTaskHandle);
            asyncTaskHandle = nullptr;
        }
    }

    delete epaper;
}

/*
 * Called by the OLEDDisplay::init() path.
 */
bool EInkParallelDisplay::connect()
{
    LOG_INFO("Do EPD init");
    int initRc = BBEP_SUCCESS;
    if (!epaper) {
        epaper = new FASTEPD;
#if defined(T5_S3_EPAPER_PRO_V1)
        initRc = epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000);
#elif defined(T5_S3_EPAPER_PRO_V2)
        initRc = epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000);
        // initialize all port 0 pins (0-7) as outputs / HIGH
        for (int i = 0; i < 8; i++) {
            epaper->ioPinMode(i, OUTPUT);
            epaper->ioWrite(i, HIGH);
        }
#else
#error "unsupported EPD device!"
#endif
    }

    // FastEPD allocates its framebuffer only from PSRAM; if PSRAM init failed the alloc returns
    // NULL and initPanel() returns an error, so clearWhite() below would memset(NULL). Skip EInk
    // bring-up but return true so OLEDDisplay still allocates its base buffer (base draw ops stay
    // safe); displayReady stays false so the FastEPD push paths no-op -> node runs headless.
    if (initRc != BBEP_SUCCESS || epaper->currentBuffer() == nullptr) {
        LOG_ERROR("EPD framebuffer unavailable (initPanel rc=%d, PSRAM=%u); running headless", initRc,
                  (unsigned)ESP.getPsramSize());
        return true;
    }

    // epaper->setRotation(rotation); // does not work, messes up width/height
    epaper->setMode(BB_MODE_1BPP);
    epaper->clearWhite();
    epaper->fullUpdate(true);

#ifdef EINK_LIMIT_GHOSTING_PX
    // After a full/clear the dirty tracking should be reset
    resetGhostPixelTracking();
#endif

    displayReady = true;
    return true;
}

/*
 * sendCommand - simple passthrough (not required for epd_driver-based path)
 */
void EInkParallelDisplay::sendCommand(uint8_t com)
{
    LOG_DEBUG("EInkParallelDisplay::sendCommand %d", (int)com);
}

/*
 * Start a background task that will perform a blocking fullUpdate(). This lets
 * display() return quickly while the heavy refresh runs in the background.
 */
void EInkParallelDisplay::startAsyncFullUpdate(int clearMode)
{
    if (asyncFullRunning.load())
        return; // already running

    asyncFullRunning.store(true);
    // pass 'this' as parameter
    BaseType_t rc = xTaskCreatePinnedToCore(EInkParallelDisplay::asyncFullUpdateTask, "epd_full", 4096 / sizeof(StackType_t),
                                            this, 2, &asyncTaskHandle,
#if CONFIG_FREERTOS_UNICORE
                                            0
#else
                                            1
#endif
    );
    if (rc != pdPASS) {
        LOG_WARN("Failed to create async full-update task, falling back to blocking update");
        epaper->fullUpdate(clearMode, false);
        epaper->backupPlane();
        asyncFullRunning.store(false);
        asyncTaskHandle = nullptr;
    }
}

/*
 * FreeRTOS task entry: runs the full update and then backs up plane.
 */
void EInkParallelDisplay::asyncFullUpdateTask(void *pvParameters)
{
    EInkParallelDisplay *self = static_cast<EInkParallelDisplay *>(pvParameters);
    if (!self) {
        vTaskDelete(nullptr);
        return;
    }

    // choose CLEAR_SLOW occasionally
    int clearMode = CLEAR_FAST;
    if (self->fastRefreshCount >= EPD_FULLSLOW_PERIOD) {
        clearMode = CLEAR_SLOW;
        self->fastRefreshCount = 0;
    } else {
        // when running async full, treat it as a full so reset fast count
        self->fastRefreshCount = 0;
    }

    self->epaper->fullUpdate(clearMode, false);
    self->epaper->backupPlane();

#ifdef EINK_LIMIT_GHOSTING_PX
    // A full refresh clears ghosting state
    self->resetGhostPixelTracking();
#endif

    self->asyncFullRunning.store(false);
    self->asyncTaskHandle = nullptr;

    // delete this task
    vTaskDelete(nullptr);
}

/*
 * Convert the OLEDDisplay buffer (vertical byte layout) into the 1bpp horizontal-bytes
 * buffer used by the FASTEPD library. For performance we write directly into FASTEPD's
 * currentBuffer() while comparing against previousBuffer() to detect changed rows.
 * After conversion we call FASTEPD::partialUpdate() or FASTEPD::fullUpdate() according
 * to a heuristic so only the minimal region is refreshed.
 */
void EInkParallelDisplay::display(void)
{
    if (!displayReady) // no framebuffer (PSRAM absent / init failed) -> nothing to push
        return;

    const uint16_t w = this->displayWidth;
    const uint16_t h = this->displayHeight;

    // Simple rate limiting: avoid very-frequent responsive updates
    uint32_t nowMs = millis();
    if (lastUpdateMs != 0 && (nowMs - lastUpdateMs) < EPD_RESPONSIVE_MIN_MS) {
        LOG_DEBUG("rate-limited, skipping update");
        return;
    }

    // bytes per row in epd format (one byte = 8 horizontal pixels)
    const uint32_t rowBytes = (w + 7) / 8;

    // Get pointers to internal buffers
    uint8_t *cur = epaper->currentBuffer();
    const uint8_t *prev = epaper->previousBuffer(); // may be NULL on first init

    // Track changed row range while converting
    int newTop = h;     // min changed row (initialized to out-of-range)
    int newBottom = -1; // max changed row

#ifdef FAST_EPD_PARTIAL_UPDATE_BUG
    // Track changed byte column range (for clipped fullUpdate fallback)
    int newLeftByte = (int)rowBytes;
    int newRightByte = -1;
#endif

    // Compute a quick hash of the incoming OLED buffer (so we can skip identical frames)
    uint32_t imageHash = 0;
    uint32_t bufBytes = (w / 8) * h; // vertical-byte layout size
    for (uint32_t bi = 0; bi < bufBytes; ++bi) {
        imageHash ^= ((uint32_t)buffer[bi]) << (bi & 31);
    }
    if (imageHash == previousImageHash) {
        // LOG_DEBUG("image identical to previous, skipping update");
        return;
    }

#ifdef EINK_LIMIT_GHOSTING_PX
    // reset ghost count for this conversion pass; we'll mark bits that change
    ghostPixelCount = 0;
#endif

    // Convert: OLED buffer layout -> FASTEPD 1bpp horizontal-bytes layout into cur,
    // comparing against prev when available to detect changes.
    for (uint32_t y = 0; y < h; ++y) {
        const uint32_t base = (y >> 3) * w;               // (y/8) * width
        const uint8_t bitMask = (uint8_t)(1u << (y & 7)); // mask for this row in vertical-byte layout
        const uint32_t rowBase = y * rowBytes;

        // process full 8-pixel bytes
        for (uint32_t xb = 0; xb < rowBytes; ++xb) {
            uint32_t x0 = xb * 8;
            // read up to 8 source bytes (vertical-byte per column)
            uint8_t b0 = (x0 + 0 < w) ? buffer[base + x0 + 0] : 0;
            uint8_t b1 = (x0 + 1 < w) ? buffer[base + x0 + 1] : 0;
            uint8_t b2 = (x0 + 2 < w) ? buffer[base + x0 + 2] : 0;
            uint8_t b3 = (x0 + 3 < w) ? buffer[base + x0 + 3] : 0;
            uint8_t b4 = (x0 + 4 < w) ? buffer[base + x0 + 4] : 0;
            uint8_t b5 = (x0 + 5 < w) ? buffer[base + x0 + 5] : 0;
            uint8_t b6 = (x0 + 6 < w) ? buffer[base + x0 + 6] : 0;
            uint8_t b7 = (x0 + 7 < w) ? buffer[base + x0 + 7] : 0;

            // build output byte: MSB = leftmost pixel
            uint8_t out = 0;
            out |= (uint8_t)((b0 & bitMask) ? 0x80 : 0x00);
            out |= (uint8_t)((b1 & bitMask) ? 0x40 : 0x00);
            out |= (uint8_t)((b2 & bitMask) ? 0x20 : 0x00);
            out |= (uint8_t)((b3 & bitMask) ? 0x10 : 0x00);
            out |= (uint8_t)((b4 & bitMask) ? 0x08 : 0x00);
            out |= (uint8_t)((b5 & bitMask) ? 0x04 : 0x00);
            out |= (uint8_t)((b6 & bitMask) ? 0x02 : 0x00);
            out |= (uint8_t)((b7 & bitMask) ? 0x01 : 0x00);

            // handle partial byte at end of row by masking off invalid bits
            uint8_t mask = 0xFF;
            uint32_t bitsRemain = (w > x0) ? (w - x0) : 0;
            if (bitsRemain > 0 && bitsRemain < 8) {
                mask = (uint8_t)(0xFF << (8 - bitsRemain));
                out &= mask;
            }

            // invert to FASTEPD polarity
            out = (~out) & mask;

            uint32_t pos = rowBase + xb;
            uint8_t prevVal = prev ? (prev[pos] & mask) : 0x00;
            // Consider this byte changed if previous buffer differs (or prev is null)
            bool changed = (prev == nullptr) || (prevVal != out);

#ifdef EINK_LIMIT_GHOSTING_PX
            if (changed && prev)
                markDirtyBits(prev, pos, mask, out);
#endif

            // mark row changed only if the previous buffer differs
            if (changed) {
                if (y < (uint32_t)newTop)
                    newTop = y;
                if ((int)y > newBottom)
                    newBottom = y;
#ifdef FAST_EPD_PARTIAL_UPDATE_BUG
                // record changed column bytes
                if ((int)xb < newLeftByte)
                    newLeftByte = (int)xb;
                if ((int)xb > newRightByte)
                    newRightByte = (int)xb;
#endif
            }

            // Always write the computed value into the current buffer (avoid leaving stale bytes)
            cur[pos] = (cur[pos] & ~mask) | out;
        }
    }

    // If nothing changed, avoid any panel update
    if (newBottom < 0) {
        LOG_DEBUG("no pixel changes detected, skipping update (conv)");
        previousImageHash = imageHash; // still remember that frame
        return;
    }

    // Choose partial vs full update using heuristic
    // Decide if we should force a full update after many fast updates
    bool forceFull = (fastRefreshCount >= EPD_FULLSLOW_PERIOD);

#ifdef EINK_LIMIT_GHOSTING_PX
    // If ghost pixels exceed limit, force a full update to clear ghosting
    if (ghostPixelCount > ghostPixelLimit) {
        LOG_WARN("ghost pixels %u > limit %u, forcing full refresh", ghostPixelCount, ghostPixelLimit);
        forceFull = true;
    }
#endif

    // Compute pixel bounds from newTop/newBottom
    int startRow = (newTop / 8) * 8;
    int endRow = (newBottom / 8) * 8 + 7;

    LOG_DEBUG("EPD update rows=%d..%d alignedRows=%d..%d rowBytes=%u", newTop, newBottom, startRow, endRow, rowBytes);

    if (epaper->getMode() == BB_MODE_1BPP && !forceFull && (newBottom - newTop) <= EPD_PARTIAL_THRESHOLD_ROWS) {
        // Prefer partial update path if driver is reliable; otherwise use clipped fullUpdate fallback.
#ifdef FAST_EPD_PARTIAL_UPDATE_BUG
        // Workaround for FastEPD partial update bug: use clipped fullUpdate instead
        // Build a pixel rectangle for a clipped fullUpdate using the changed columns
        int startCol = (newLeftByte <= newRightByte) ? (newLeftByte * 8) : 0;
        int endCol = (newLeftByte <= newRightByte) ? ((newRightByte + 1) * 8 - 1) : (w - 1);

        BB_RECT rect{startCol, startRow, endCol - startCol + 1, endRow - startRow + 1};
        // LOG_DEBUG("Using clipped fullUpdate rect x=%d y=%d w=%d h=%d", rect.x, rect.y, rect.w, rect.h);
        epaper->fullUpdate(CLEAR_FAST, false, &rect);
#else
        // Use rows for partial update
        LOG_DEBUG("calling partialUpdate startRow=%d endRow=%d", startRow, endRow);
        epaper->partialUpdate(true, startRow, endRow);
#endif
        epaper->backupPlane();
        fastRefreshCount++;
    } else {
        // Full update: run async if possible (startAsyncFullUpdate will fall back to blocking)
        startAsyncFullUpdate(forceFull ? CLEAR_SLOW : CLEAR_FAST);
    }

    lastUpdateMs = millis();
    previousImageHash = imageHash;

    // Keep same behavior as before
    lastDrawMsec = millis();
}

#ifdef EINK_LIMIT_GHOSTING_PX
// markDirtyBits: mark per-bit dirty flags and update ghostPixelCount
void EInkParallelDisplay::markDirtyBits(const uint8_t *prevBuf, uint32_t pos, uint8_t mask, uint8_t out)
{
    // defensive: need dirtyPixels allocated and prevBuf valid
    if (!dirtyPixels || !prevBuf)
        return;

    // 'out' is in FASTEPD polarity (1 = black, 0 = white)
    uint8_t newBlack = out & mask;    // bits that will be black now
    uint8_t newWhite = (~out) & mask; // bits that will be white now

    // previously recorded dirty bits for this byte
    uint8_t before = dirtyPixels[pos];

    // Ghost bits: bits that were previously marked dirty and are now being driven white
    uint8_t ghostBits = before & newWhite;
    if (ghostBits) {
        ghostPixelCount += __builtin_popcount((unsigned)ghostBits);
    }

    // Only mark bits dirty when they turn black now (accumulate until a full refresh)
    uint8_t newlyDirty = newBlack & (~before);
    if (newlyDirty) {
        dirtyPixels[pos] |= newlyDirty;
    }
}

// reset ghost tracking (call after a full refresh)
void EInkParallelDisplay::resetGhostPixelTracking()
{
    if (!dirtyPixels)
        return;
    memset(dirtyPixels, 0, dirtyPixelsSize);
    ghostPixelCount = 0;
}
#endif

/*
 * forceDisplay: use lastDrawMsec
 */
bool EInkParallelDisplay::forceDisplay(uint32_t msecLimit)
{
    if (!displayReady)
        return false;

    uint32_t now = millis();
    if (lastDrawMsec == 0 || (now - lastDrawMsec) > msecLimit) {
        display();
        return true;
    }
    return false;
}

void EInkParallelDisplay::endUpdate()
{
    if (!displayReady)
        return;
    {
        // ensure any async full update is started/completed
        if (asyncFullRunning.load()) {
            // nothing to do; background task will run and call backupPlane when done
        } else {
            epaper->fullUpdate(CLEAR_FAST, false);
            epaper->backupPlane();
#ifdef EINK_LIMIT_GHOSTING_PX
            resetGhostPixelTracking();
#endif
        }
    }
}

#endif