#include "pxt.h"
#include "ST7735.h"
#include "ILI9341.h"

#include "SPIScreenIO.h"
#ifdef STM32F4
#include "FSMCIO.h"
#endif

#include "jddisplay.h"

namespace pxt {

class WDisplay {
  public:
    ScreenIO *io;
    ST7735 *lcd;
#ifdef ARCADE_MBIT_CODAL
    JDDisplay *smart;
#else
    void* smart;
#endif
    uint32_t currPalette[16];
    bool newPalette;
    bool inUpdate;

    uint8_t *screenBuf;
    Image_ lastStatus;

    uint16_t width, height;
    uint16_t displayHeight;
    uint8_t offX, offY;
    bool doubleSize;
    uint32_t palXOR;

#ifdef USE_RGB444
    // RGB444 streaming path for ST7735 panels lacking the RGBSET LUT (see #6861)
    SPI *spi_ = NULL;
    Pin *dcPin_ = NULL;
    Pin *csPin_ = NULL;
    bool rgb444_ = false;
    uint8_t pal4R_[16], pal4G_[16], pal4B_[16];

    inline void wrCmd1(uint8_t cmd, uint8_t a0) {
        dcPin_->setDigitalValue(0); csPin_->setDigitalValue(0); spi_->write(cmd);
        dcPin_->setDigitalValue(1); spi_->write(a0);
        csPin_->setDigitalValue(1);
    }
#endif

    WDisplay() {
        uint32_t cfg2 = getConfig(CFG_DISPLAY_CFG2, 0x0);
        int conn = cfg2 >> 24;

        uint32_t cfg0 = getConfig(CFG_DISPLAY_CFG0, 0x40);
        uint32_t frmctr1 = getConfig(CFG_DISPLAY_CFG1, 0x000603);

        int dispTp = getConfig(CFG_DISPLAY_TYPE, DISPLAY_TYPE_ST7735);

        doubleSize = false;
        smart = NULL;

        auto miso = LOOKUP_PIN(DISPLAY_MISO);

        if (dispTp == DISPLAY_TYPE_SMART) {
            dispTp = smartConfigure(&cfg0, &frmctr1, &cfg2);
        }

        if (dispTp != DISPLAY_TYPE_SMART)
            miso = NULL; // only JDDisplay needs MISO, otherwise leave free

        SPI *spi = NULL;
        if (conn == 0) {
#ifdef ARCADE_MBIT_CODAL
            NRF52Pin* mosi = LOOKUP_PIN(DISPLAY_MOSI);
            NRF52Pin* sck = LOOKUP_PIN(DISPLAY_SCK);
            mosi->setHighDrive(true);
            sck->setHighDrive(true);
            spi = new CODAL_SPI(*mosi, *miso, *sck);
#else
            spi = new CODAL_SPI(*LOOKUP_PIN(DISPLAY_MOSI), *miso, *LOOKUP_PIN(DISPLAY_SCK));
#endif
            io = new SPIScreenIO(*spi);
#ifdef USE_RGB444
            spi_ = spi;
            dcPin_ = LOOKUP_PIN(DISPLAY_DC);
            csPin_ = LOOKUP_PIN(DISPLAY_CS);
#endif
        } else if (conn == 1) {
#ifdef CODAL_CREATE_PARALLEL_SCREEN_IO
            io = CODAL_CREATE_PARALLEL_SCREEN_IO(cfg2 & 0xffffff, PIN(DISPLAY_MOSI),
                                                 PIN(DISPLAY_MISO));
#else
            target_panic(PANIC_SCREEN_ERROR);
#endif
        } else {
            target_panic(PANIC_SCREEN_ERROR);
        }

#ifdef USE_RGB444
        // Runtime scope: RGB444 only on known Adafruit boards. Current Adafruit
        // bootloaders publish VID:PID (VID 0x239A) as the board-id; older ones
        // (which new boards still ship with) published fixed per-model ids, listed
        // below. Unknown or absent ids keep the stock RGBSET path (e.g. Kitronik).
        uint32_t boardId = (uint32_t)getConfig(CFG_BOOTLOADER_BOARD_ID, 0);
        rgb444_ = spi_ && ((boardId >> 16) == 0x239A || // current Adafruit (VID:PID)
                           boardId == 0x18591ab9 ||     // PyBadge/LC/PyGamer (2019)
                           boardId == 0x75fdeb5f ||     // PyBadge
                           boardId == 0x3f05ba69 ||     // PyBadge LC
                           boardId == 0x2dd7a88c ||     // PyGamer
                           boardId == 0x2b9e3d05 ||     // Feather M4 Arcade
                           boardId == 0x7a236324);      // ItsyBitsy M4 Arcade
#endif

        if (dispTp == DISPLAY_TYPE_ST7735)
            lcd = new ST7735(*io, *LOOKUP_PIN(DISPLAY_CS), *LOOKUP_PIN(DISPLAY_DC));
        else if (dispTp == DISPLAY_TYPE_ILI9341) {
            lcd = new ILI9341(*io, *LOOKUP_PIN(DISPLAY_CS), *LOOKUP_PIN(DISPLAY_DC));
            doubleSize = true;
#ifdef ARCADE_MBIT_CODAL
        } else if (dispTp == DISPLAY_TYPE_SMART) {
            lcd = NULL;
            smart = new JDDisplay(spi, LOOKUP_PIN(DISPLAY_CS), LOOKUP_PIN(DISPLAY_DC));
#endif
        } else
            target_panic(PANIC_SCREEN_ERROR);

        palXOR = (cfg0 & 0x1000000) ? 0xffffff : 0x000000;
        auto madctl = cfg0 & 0xff;
        offX = (cfg0 >> 8) & 0xff;
        offY = (cfg0 >> 16) & 0xff;

        DMESG("configure screen: FRMCTR1=%p MADCTL=%p type=%d", frmctr1, madctl, dispTp);

        if (spi) {
            auto freq = (cfg2 & 0xff);
            if (!freq)
                freq = 15;
            spi->setFrequency(freq * 1000000);
            spi->setMode(0);
            auto cs = LOOKUP_PIN(DISPLAY_CS);
            if (cs)
                cs->setDigitalValue(1);

            // make sure the SPI peripheral is initialized before toggling reset
            spi->write(0);
        }

        auto rst = LOOKUP_PIN(DISPLAY_RST);
        if (rst) {
            rst->setDigitalValue(0);
            fiber_sleep(20);
            rst->setDigitalValue(1);
            fiber_sleep(20);
        }

        if (lcd) {
            auto bl = LOOKUP_PIN(DISPLAY_BL);
            if (bl) {
                bl->setDigitalValue(1);
            }

            lcd->init();
            lcd->configure(madctl, frmctr1);

#ifdef USE_RGB444
            if (rgb444_ && !doubleSize) {
                // 12bpp RGB444 mode: lets us stream color without the RGBSET LUT
                wrCmd1(0x3A, 0x03); // COLMOD
                wrCmd1(0x20, 0x00); // INVOFF
            }
#endif
        }

        width = getConfig(CFG_DISPLAY_WIDTH, 160);
        height = getConfig(CFG_DISPLAY_HEIGHT, 128);
        displayHeight = height;
        setAddrMain();
        DMESG("screen: %d x %d, off=%d,%d", width, height, offX, offY);
        int sz = doubleSize ? (width >> 1) * (height >> 1) : width * height;
        screenBuf = (uint8_t *)app_alloc(sz / 2 + 20);

        lastStatus = NULL;
        registerGC((TValue *)&lastStatus);
        inUpdate = false;
    }

    uint32_t smartConfigure(uint32_t *cfg0, uint32_t *cfg1, uint32_t *cfg2) {
        uint32_t hc;

        DMESG("74HC: waiting...");

        // wait while nothing is connected
        for (;;) {
            auto rst = LOOKUP_PIN(DISPLAY_RST);
            if (rst) {
                rst->setDigitalValue(0);
                target_wait_us(10);
                rst->setDigitalValue(1);
                fiber_sleep(3); // in reality we need around 1.2ms
            }

            hc = readButtonMultiplexer(17);
            if (hc != 0)
                break;

            fiber_sleep(100);

            // the device will run without shield when the following is specified in user program:
            // namespace userconfig { export const DISPLAY_CFG0 = 0x02000080 }
            if (*cfg0 & 0x2000000) {
                DMESG("74HC: no wait requested");
                return DISPLAY_TYPE_ST7735;
            }
        }

        DMESG("74HC: %x", hc);

        // is the line forced up? if so, assume JDDisplay
        if (hc == 0x1FFFF) {
            disableButtonMultiplexer();
            return DISPLAY_TYPE_SMART;
        }

        hc = hc >> 1;

        // SER pin (or first bit of second HC) is orientation
        if (hc & 0x0010)
            *cfg0 = ((*cfg0) & 0xffffff3f) | 0x80;
        else
            *cfg0 = ((*cfg0) & 0xffffff3f) | 0x40;

        uint32_t configId = (hc & 0xe0) >> 5;

        switch (configId) {
        case 1:
            *cfg1 = 0x0603; // ST7735
            break;
        case 2:
            *cfg1 = 0xe14ff; // ILI9163C
            *cfg0 |= 0x08;   // BGR colors
            break;
        case 3:
            *cfg1 = 0x0603;     // ST7735
            *cfg0 |= 0x1000000; // inverted colors
            break;
        default:
            target_panic(PANIC_SCREEN_ERROR);
            break;
        }

        DMESG("config type: %d; cfg0=%x cfg1=%x", configId, *cfg0, *cfg1);
        *cfg2 = getConfig(CFG_CLOCK_SPEED, 32);

        return DISPLAY_TYPE_ST7735;
    }

    void setAddrStatus() {
        if (lcd)
            lcd->setAddrWindow(offX, offY + displayHeight, width, height - displayHeight);
#ifdef ARCADE_MBIT_CODAL
        else
            smart->setAddrWindow(offX, offY + displayHeight, width, height - displayHeight);
#endif
    }
    void setAddrMain() {
        if (lcd)
            lcd->setAddrWindow(offX, offY, width, displayHeight);
#ifdef ARCADE_MBIT_CODAL
        else
            smart->setAddrWindow(offX, offY, width, displayHeight);
#endif
    }
    void waitForSendDone() {
        if (lcd)
            lcd->waitForSendDone();
#ifdef ARCADE_MBIT_CODAL
        else
            smart->waitForSendDone();
#endif
    }
    int sendIndexedImage(const uint8_t *src, unsigned width, unsigned height, uint32_t *palette) {
        if (lcd)
            return lcd->sendIndexedImage(src, width, height, palette);
#ifdef ARCADE_MBIT_CODAL
        else
            return smart->sendIndexedImage(src, width, height, palette);
#else
        return 0;
#endif
    }

#ifdef USE_RGB444
    // Stream a 4bpp indexed image as RGB444 over SPI (replaces the RGBSET LUT path).
    // Batches up to 3 lines per SPI transfer to reduce per-transfer overhead.
    void sendIndexedImage444(const uint8_t *src, int W, int H) {
        static uint8_t buf444[3 * (3 * 160) / 2]; // 160 = max ST7735 width
        const int bytesPerRow = (W + 1) >> 1;
        dcPin_->setDigitalValue(0);
        csPin_->setDigitalValue(0);
        spi_->write(0x2C); // RAMWR
        dcPin_->setDigitalValue(1);
        int out = 0;
        for (int y = 0; y < H; y++) {
            const uint8_t *row = src + y * bytesPerRow;
            for (int x = 0; x < W; x += 2) {
                uint8_t b = *row++;
                uint8_t i0 = b & 0x0F, i1 = (b >> 4) & 0x0F;
                buf444[out++] = (pal4R_[i0] << 4) | pal4G_[i0];
                buf444[out++] = (pal4B_[i0] << 4) | pal4R_[i1];
                buf444[out++] = (pal4G_[i1] << 4) | pal4B_[i1];
            }
            if (out + 3 * (W + 1) / 2 > (int)sizeof(buf444) || y == H - 1) {
                spi_->transfer((uint8_t *)buf444, out, NULL, 0);
                out = 0;
            }
        }
        csPin_->setDigitalValue(1);
    }
#endif
};

SINGLETON_IF_PIN(WDisplay, DISPLAY_MOSI);

//%
int setScreenBrightnessSupported() {
    auto display = getWDisplay();
    if (display && display->smart)
        return 1;

    auto bl = LOOKUP_PIN(DISPLAY_BL);
    if (!bl)
        return 0;
#ifdef SAMD51
    if (bl->name == PA06)
        return 0;
#endif
#ifdef NRF52_SERIES
    // PWM not implemented yet
    return 0;
#else
    return 1;
#endif
}

//%
void setScreenBrightness(int level) {
    if (level < 0)
        level = 0;
    if (level > 100)
        level = 100;

    auto display = getWDisplay();
    if (display && display->smart) {
#ifdef ARCADE_MBIT_CODAL
        display->smart->brightness = level;
#endif
        return;
    }

    auto bl = LOOKUP_PIN(DISPLAY_BL);
    if (!bl)
        return;

    if (level == 0)
        bl->setDigitalValue(0);
    else if (level == 100)
        bl->setDigitalValue(1);
    else {
        if (setScreenBrightnessSupported()) {
            bl->setAnalogPeriodUs(1000);
            bl->setAnalogValue(level * level * 1023 / 10000);
        }
    }
}

//%
void setPalette(Buffer buf) {
    auto display = getWDisplay();
    if (!display)
        return;

    if (48 != buf->length)
        target_panic(PANIC_SCREEN_ERROR);
    for (int i = 0; i < 16; ++i) {
        display->currPalette[i] =
            (buf->data[i * 3] << 16) | (buf->data[i * 3 + 1] << 8) | (buf->data[i * 3 + 2] << 0);
        display->currPalette[i] ^= display->palXOR;
#ifdef USE_RGB444
        display->pal4R_[i] = (display->currPalette[i] >> 20) & 0xF;
        display->pal4G_[i] = (display->currPalette[i] >> 12) & 0xF;
        display->pal4B_[i] = (display->currPalette[i] >> 4) & 0xF;
#endif
    }
    display->newPalette = true;
}

//%
void setupScreenStatusBar(int barHeight) {
    auto display = getWDisplay();
    if (!display)
        return;
    if (!display->doubleSize) {
        display->displayHeight = display->height - barHeight;
        display->setAddrMain();
    }
}

//%
void updateScreenStatusBar(Image_ img) {
    auto display = getWDisplay();
    if (!display)
        return;

    if (!img)
        return;
    display->lastStatus = img;
}

//%
void updateScreen(Image_ img) {
    auto display = getWDisplay();
    if (!display)
        return;

    if (display->inUpdate)
        return;

    display->inUpdate = true;

    auto mult = display->doubleSize ? 2 : 1;

    if (img) {
        if (img->bpp() != 4 || img->width() * mult != display->width ||
            img->height() * mult != display->displayHeight)
            target_panic(PANIC_SCREEN_ERROR);

        // DMESG("wait for done");
        display->waitForSendDone();

#ifdef USE_RGB444
        if (display->rgb444_ && display->lcd && !display->doubleSize) {
            memcpy(display->screenBuf, img->pix(), img->pixLength());
            display->setAddrMain();
            display->sendIndexedImage444(display->screenBuf, display->width, display->displayHeight);
        } else
#endif
        {
            auto palette = display->currPalette;

            if (display->newPalette) {
                display->newPalette = false;
            } else {
                // smart mode always sends palette
                if (!display->smart)
                    palette = NULL;
            }

            memcpy(display->screenBuf, img->pix(), img->pixLength());

            // DMESG("send");
            display->sendIndexedImage(display->screenBuf, img->width(), img->height(), palette);
        }
    }

    if (display->lastStatus && !display->doubleSize && !display->smart) {
        display->waitForSendDone();
        img = display->lastStatus;
        auto barHeight = display->height - display->displayHeight;
        if (img->bpp() != 4 || barHeight != img->height() || img->width() != display->width)
            target_panic(PANIC_SCREEN_ERROR);
        memcpy(display->screenBuf, img->pix(), img->pixLength());
        display->setAddrStatus();
#ifdef USE_RGB444
        if (display->rgb444_) {
            display->sendIndexedImage444(display->screenBuf, display->width, barHeight);
        } else
#endif
        {
            display->sendIndexedImage(display->screenBuf, img->width(), img->height(), NULL);
            display->waitForSendDone();
        }
        display->setAddrMain();
        display->lastStatus = NULL;
    }

    display->inUpdate = false;
}

//%
void updateStats(String msg) {
    // ignore...
}

} // namespace pxt
