#include "M5Display.h"

#ifdef M5Stack_M5Core2
#include <M5Touch.h>
#endif /* M5Stack_M5Core2 */

#define BLK_PWM_CHANNEL 7  // LEDC_CHANNEL_7

// So we can use this instance without including all of M5Core2 / M5Stack
M5Display *M5Display::instance;

M5Display::M5Display() : TFT_eSPI() {
    if (!instance) instance = this;
}

void M5Display::begin() {
    TFT_eSPI::begin();
    setRotation(1);
    fillScreen(0);

    // Init the back-light LED PWM
    ledcSetup(BLK_PWM_CHANNEL, 44100, 8);
    // ledcAttachPin(TFT_BL, BLK_PWM_CHANNEL);
    ledcWrite(BLK_PWM_CHANNEL, 80);
}

void M5Display::sleep() {
    startWrite();
    writecommand(ILI9341_SLPIN);  // Software reset
    endWrite();
}

void M5Display::wakeup() {
    startWrite();
    writecommand(ILI9341_SLPOUT);
    endWrite();
}

void M5Display::setBrightness(uint8_t brightness) {
    ledcWrite(BLK_PWM_CHANNEL, brightness);
}

void M5Display::drawBitmap(int16_t x0, int16_t y0, int16_t w, int16_t h,
                           const uint16_t *data) {
    bool swap = getSwapBytes();
    setSwapBytes(true);
    pushImage((int32_t)x0, (int32_t)y0, (uint32_t)w, (uint32_t)h, data);
    setSwapBytes(swap);
}

void M5Display::drawBitmap(int16_t x0, int16_t y0, int16_t w, int16_t h,
                           uint16_t *data) {
    bool swap = getSwapBytes();
    setSwapBytes(true);
    pushImage((int32_t)x0, (int32_t)y0, (uint32_t)w, (uint32_t)h, data);
    setSwapBytes(swap);
}

void M5Display::drawBitmap(int16_t x0, int16_t y0, int16_t w, int16_t h,
                           const uint16_t *data, uint16_t transparent) {
    bool swap = getSwapBytes();
    setSwapBytes(true);
    pushImage((int32_t)x0, (int32_t)y0, (uint32_t)w, (uint32_t)h, data,
              transparent);
    setSwapBytes(swap);
}

void M5Display::drawBitmap(int16_t x0, int16_t y0, int16_t w, int16_t h,
                           const uint8_t *data) {
    bool swap = getSwapBytes();
    setSwapBytes(true);
    pushImage((int32_t)x0, (int32_t)y0, (uint32_t)w, (uint32_t)h,
              (const uint16_t *)data);
    setSwapBytes(swap);
}

void M5Display::drawBitmap(int16_t x0, int16_t y0, int16_t w, int16_t h,
                           uint8_t *data) {
    bool swap = getSwapBytes();
    setSwapBytes(true);
    pushImage((int32_t)x0, (int32_t)y0, (uint32_t)w, (uint32_t)h,
              (uint16_t *)data);
    setSwapBytes(swap);
}

void M5Display::progressBar(int x, int y, int w, int h, uint8_t val) {
    drawRect(x, y, w, h, 0x09F1);
    fillRect(x + 1, y + 1, w * (((float)val) / 100.0), h - 1, 0x09F1);
}

#include "utility/qrcode.h"
void M5Display::qrcode(const char *string, uint16_t x, uint16_t y,
                       uint8_t width, uint8_t version) {
    // Create the QR code
    QRCode qrcode;
    uint8_t qrcodeData[qrcode_getBufferSize(version)];
    qrcode_initText(&qrcode, qrcodeData, version, 0, string);

    // Top quiet zone
    uint8_t thickness   = width / qrcode.size;
    uint16_t lineLength = qrcode.size * thickness;
    uint8_t xOffset     = x + (width - lineLength) / 2;
    uint8_t yOffset     = y + (width - lineLength) / 2;
    fillRect(x, y, width, width, TFT_WHITE);

    for (uint8_t y = 0; y < qrcode.size; y++) {
        for (uint8_t x = 0; x < qrcode.size; x++) {
            uint8_t q = qrcode_getModule(&qrcode, x, y);
            if (q)
                fillRect(x * thickness + xOffset, y * thickness + yOffset,
                         thickness, thickness, TFT_BLACK);
        }
    }
}

void M5Display::qrcode(const String &string, uint16_t x, uint16_t y,
                       uint8_t width, uint8_t version) {
    int16_t len = string.length() + 2;
    char buffer[len];
    string.toCharArray(buffer, len);
    qrcode(buffer, x, y, width, version);
}

// These read 16- and 32-bit types from the SD card file.
// BMP data is stored little-endian, Arduino is little-endian too.
// May need to reverse subscript order if porting elsewhere.

uint16_t read16(fs::File &f) {
    uint16_t result;
    ((uint8_t *)&result)[0] = f.read();  // LSB
    ((uint8_t *)&result)[1] = f.read();  // MSB
    return result;
}

uint32_t read32(fs::File &f) {
    uint32_t result;
    ((uint8_t *)&result)[0] = f.read();  // LSB
    ((uint8_t *)&result)[1] = f.read();
    ((uint8_t *)&result)[2] = f.read();
    ((uint8_t *)&result)[3] = f.read();  // MSB
    return result;
}

// Bodmers BMP image rendering function
void M5Display::drawBmpFile(fs::FS &fs, const char *path, uint16_t x,
                            uint16_t y) {
    if ((x >= width()) || (y >= height())) return;

    // Open requested file on SD card
    File bmpFS = fs.open(path, "r");

    if (!bmpFS) {
        Serial.print("File not found");
        return;
    }

    uint32_t seekOffset;
    uint16_t w, h, row, col;
    uint8_t r, g, b;

    uint32_t startTime = millis();

    if (read16(bmpFS) == 0x4D42) {
        read32(bmpFS);
        read32(bmpFS);
        seekOffset = read32(bmpFS);
        read32(bmpFS);
        w = read32(bmpFS);
        h = read32(bmpFS);

        if ((read16(bmpFS) == 1) && (read16(bmpFS) == 24) &&
            (read32(bmpFS) == 0)) {
            y += h - 1;

            setSwapBytes(true);
            bmpFS.seek(seekOffset);

            uint16_t padding = (4 - ((w * 3) & 3)) & 3;
            uint8_t lineBuffer[w * 3 + padding];

            for (row = 0; row < h; row++) {
                bmpFS.read(lineBuffer, sizeof(lineBuffer));
                uint8_t *bptr  = lineBuffer;
                uint16_t *tptr = (uint16_t *)lineBuffer;
                // Convert 24 to 16 bit colours
                for (col = 0; col < w; col++) {
                    b       = *bptr++;
                    g       = *bptr++;
                    r       = *bptr++;
                    *tptr++ = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
                }

                // Push the pixel row to screen, pushImage will crop the line if
                // needed y is decremented as the BMP image is drawn bottom up
                pushImage(x, y--, w, 1, (uint16_t *)lineBuffer);
            }
            Serial.print("Loaded in ");
            Serial.print(millis() - startTime);
            Serial.println(" ms");
        } else
            Serial.println("BMP format not recognized.");
    }
    bmpFS.close();
}

// void M5Display::drawBmp(fs::FS &fs, const char *path, uint16_t x, uint16_t y)
// {
//   drawBmpFile(fs, path, x, y);
// }
/***************************************************
  This library is written to be compatible with Adafruit's ILI9341
  library and automatically detects the display type on ESP_WROVER_KITs
  Earlier WROVERs had ILI9341, while newer releases have ST7789V

  MIT license, all text above must be included in any redistribution
 ****************************************************/

/*
 * JPEG
 */

#include "rom/tjpgd.h"

#define jpgColor(c)                                  \
    (((uint16_t)(((uint8_t *)(c))[0] & 0xF8) << 8) | \
     ((uint16_t)(((uint8_t *)(c))[1] & 0xFC) << 3) | \
     ((((uint8_t *)(c))[2] & 0xF8) >> 3))

#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_ERROR
const char *jd_errors[] = {"Succeeded",
                           "Interrupted by output function",
                           "Device error or wrong termination of input stream",
                           "Insufficient memory pool for the image",
                           "Insufficient stream input buffer",
                           "Parameter error",
                           "Data format error",
                           "Right format but not supported",
                           "Not supported JPEG standard"};
#endif

typedef struct {
    uint16_t x;
    uint16_t y;
    uint16_t maxWidth;
    uint16_t maxHeight;
    uint16_t offX;
    uint16_t offY;
    jpeg_div_t scale;
    const void *src;
    size_t len;
    size_t index;
    M5Display *tft;
    uint16_t outWidth;
    uint16_t outHeight;
} jpg_file_decoder_t;

static uint32_t jpgReadFile(JDEC *decoder, uint8_t *buf, uint32_t len) {
    jpg_file_decoder_t *jpeg = (jpg_file_decoder_t *)decoder->device;
    File *file               = (File *)jpeg->src;
    if (buf) {
        return file->read(buf, len);
    } else {
        file->seek(len, SeekCur);
    }
    return len;
}

static uint32_t jpgRead(JDEC *decoder, uint8_t *buf, uint32_t len) {
    jpg_file_decoder_t *jpeg = (jpg_file_decoder_t *)decoder->device;
    if (buf) {
        memcpy(buf, (const uint8_t *)jpeg->src + jpeg->index, len);
    }
    jpeg->index += len;
    return len;
}

static uint32_t jpgWrite(JDEC *decoder, void *bitmap, JRECT *rect) {
    jpg_file_decoder_t *jpeg = (jpg_file_decoder_t *)decoder->device;
    uint16_t x               = rect->left;
    uint16_t y               = rect->top;
    uint16_t w               = rect->right + 1 - x;
    uint16_t h               = rect->bottom + 1 - y;
    uint16_t oL = 0, oR = 0;
    uint8_t *data = (uint8_t *)bitmap;

    if (rect->right < jpeg->offX) {
        return 1;
    }
    if (rect->left >= (jpeg->offX + jpeg->outWidth)) {
        return 1;
    }
    if (rect->bottom < jpeg->offY) {
        return 1;
    }
    if (rect->top >= (jpeg->offY + jpeg->outHeight)) {
        return 1;
    }
    if (rect->top < jpeg->offY) {
        uint16_t linesToSkip = jpeg->offY - rect->top;
        data += linesToSkip * w * 3;
        h -= linesToSkip;
        y += linesToSkip;
    }
    if (rect->bottom >= (jpeg->offY + jpeg->outHeight)) {
        uint16_t linesToSkip =
            (rect->bottom + 1) - (jpeg->offY + jpeg->outHeight);
        h -= linesToSkip;
    }
    if (rect->left < jpeg->offX) {
        oL = jpeg->offX - rect->left;
    }
    if (rect->right >= (jpeg->offX + jpeg->outWidth)) {
        oR = (rect->right + 1) - (jpeg->offX + jpeg->outWidth);
    }

    uint16_t pixBuf[32];
    uint8_t pixIndex = 0;
    uint16_t line;

    jpeg->tft->startWrite();
    // jpeg->tft->setAddrWindow(x - jpeg->offX + jpeg->x + oL, y - jpeg->offY +
    // jpeg->y, w - (oL + oR), h);
    jpeg->tft->setWindow(x - jpeg->offX + jpeg->x + oL,
                         y - jpeg->offY + jpeg->y,
                         x - jpeg->offX + jpeg->x + oL + w - (oL + oR) - 1,
                         y - jpeg->offY + jpeg->y + h - 1);

    while (h--) {
        data += 3 * oL;
        line = w - (oL + oR);
        while (line--) {
            pixBuf[pixIndex++] = jpgColor(data);
            data += 3;
            if (pixIndex == 32) {
                jpeg->tft->writePixels(pixBuf, 32);
                // SPI.writePixels((uint8_t *)pixBuf, 64);
                pixIndex = 0;
            }
        }
        data += 3 * oR;
    }
    if (pixIndex) {
        jpeg->tft->writePixels(pixBuf, pixIndex);
        // SPI.writePixels((uint8_t *)pixBuf, pixIndex * 2);
    }
    jpeg->tft->endWrite();
    return 1;
}

static bool jpgDecode(jpg_file_decoder_t *jpeg,
                      uint32_t (*reader)(JDEC *, uint8_t *, uint32_t)) {
    static uint8_t work[3100];
    JDEC decoder;

    JRESULT jres = jd_prepare(&decoder, reader, work, 3100, jpeg);
    if (jres != JDR_OK) {
        log_e("jd_prepare failed! %s", jd_errors[jres]);
        return false;
    }

    uint16_t jpgWidth  = decoder.width / (1 << (uint8_t)(jpeg->scale));
    uint16_t jpgHeight = decoder.height / (1 << (uint8_t)(jpeg->scale));

    if (jpeg->offX >= jpgWidth || jpeg->offY >= jpgHeight) {
        log_e("Offset Outside of JPEG size");
        return false;
    }

    size_t jpgMaxWidth  = jpgWidth - jpeg->offX;
    size_t jpgMaxHeight = jpgHeight - jpeg->offY;

    jpeg->outWidth =
        (jpgMaxWidth > jpeg->maxWidth) ? jpeg->maxWidth : jpgMaxWidth;
    jpeg->outHeight =
        (jpgMaxHeight > jpeg->maxHeight) ? jpeg->maxHeight : jpgMaxHeight;

    jres = jd_decomp(&decoder, jpgWrite, (uint8_t)jpeg->scale);
    if (jres != JDR_OK) {
        log_e("jd_decomp failed! %s", jd_errors[jres]);
        return false;
    }

    return true;
}

void M5Display::drawJpg(const uint8_t *jpg_data, size_t jpg_len, uint16_t x,
                        uint16_t y, uint16_t maxWidth, uint16_t maxHeight,
                        uint16_t offX, uint16_t offY, jpeg_div_t scale) {
    if ((x + maxWidth) > width() || (y + maxHeight) > height()) {
        log_e("Bad dimensions given");
        return;
    }

    jpg_file_decoder_t jpeg;

    if (!maxWidth) {
        maxWidth = width() - x;
    }
    if (!maxHeight) {
        maxHeight = height() - y;
    }

    jpeg.src       = jpg_data;
    jpeg.len       = jpg_len;
    jpeg.index     = 0;
    jpeg.x         = x;
    jpeg.y         = y;
    jpeg.maxWidth  = maxWidth;
    jpeg.maxHeight = maxHeight;
    jpeg.offX      = offX;
    jpeg.offY      = offY;
    jpeg.scale     = scale;
    jpeg.tft       = this;

    jpgDecode(&jpeg, jpgRead);
}

void M5Display::drawJpgFile(fs::FS &fs, const char *path, uint16_t x,
                            uint16_t y, uint16_t maxWidth, uint16_t maxHeight,
                            uint16_t offX, uint16_t offY, jpeg_div_t scale) {
    if ((x + maxWidth) > width() || (y + maxHeight) > height()) {
        log_e("Bad dimensions given");
        return;
    }

    File file = fs.open(path);
    if (!file) {
        log_e("Failed to open file for reading");
        return;
    }

    jpg_file_decoder_t jpeg;

    if (!maxWidth) {
        maxWidth = width() - x;
    }
    if (!maxHeight) {
        maxHeight = height() - y;
    }

    jpeg.src       = &file;
    jpeg.len       = file.size();
    jpeg.index     = 0;
    jpeg.x         = x;
    jpeg.y         = y;
    jpeg.maxWidth  = maxWidth;
    jpeg.maxHeight = maxHeight;
    jpeg.offX      = offX;
    jpeg.offY      = offY;
    jpeg.scale     = scale;
    jpeg.tft       = this;

    jpgDecode(&jpeg, jpgReadFile);

    file.close();
}

/*
 * PNG
 */

#include "utility/pngle.h"
#include <HTTPClient.h>

typedef struct _png_draw_params {
    uint16_t x;
    uint16_t y;
    uint16_t maxWidth;
    uint16_t maxHeight;
    uint16_t offX;
    uint16_t offY;
    double scale;
    uint8_t alphaThreshold;

    M5Display *tft;
} png_file_decoder_t;

static void pngle_draw_callback(pngle_t *pngle, uint32_t x, uint32_t y,
                                uint32_t w, uint32_t h, uint8_t rgba[4]) {
    png_file_decoder_t *p = (png_file_decoder_t *)pngle_get_user_data(pngle);
    uint16_t color        = jpgColor(rgba);  // XXX: It's PNG ;)

    if (x < p->offX || y < p->offY) return;
    x -= p->offX;
    y -= p->offY;

    // An interlaced file with alpha channel causes disaster, so use 1 here for
    // simplicity
    w = 1;
    h = 1;

    if (p->scale != 1.0) {
        x = (uint32_t)ceil(x * p->scale);
        y = (uint32_t)ceil(y * p->scale);
        w = (uint32_t)ceil(w * p->scale);
        h = (uint32_t)ceil(h * p->scale);
    }

    if (x >= p->maxWidth || y >= p->maxHeight) return;
    if (x + w >= p->maxWidth) w = p->maxWidth - x;
    if (y + h >= p->maxHeight) h = p->maxHeight - y;

    x += p->x;
    y += p->y;

    if (rgba[3] >= p->alphaThreshold) {
        p->tft->fillRect(x, y, w, h, color);
    }
}

void M5Display::drawPngFile(fs::FS &fs, const char *path, uint16_t x,
                            uint16_t y, uint16_t maxWidth, uint16_t maxHeight,
                            uint16_t offX, uint16_t offY, double scale,
                            uint8_t alphaThreshold) {
    File file = fs.open(path);
    if (!file) {
        log_e("Failed to open file for reading");
        return;
    }

    pngle_t *pngle = pngle_new();

    png_file_decoder_t png;

    if (!maxWidth) {
        maxWidth = width() - x;
    }
    if (!maxHeight) {
        maxHeight = height() - y;
    }

    png.x              = x;
    png.y              = y;
    png.maxWidth       = maxWidth;
    png.maxHeight      = maxHeight;
    png.offX           = offX;
    png.offY           = offY;
    png.scale          = scale;
    png.alphaThreshold = alphaThreshold;
    png.tft            = this;

    pngle_set_user_data(pngle, &png);
    pngle_set_draw_callback(pngle, pngle_draw_callback);

    // Feed data to pngle
    uint8_t buf[1024];
    int remain = 0;
    int len;
    while ((len = file.read(buf + remain, sizeof(buf) - remain)) > 0) {
        int fed = pngle_feed(pngle, buf, remain + len);
        if (fed < 0) {
            log_e("[pngle error] %s", pngle_error(pngle));
            break;
        }

        remain = remain + len - fed;
        if (remain > 0) memmove(buf, buf + fed, remain);
    }

    pngle_destroy(pngle);
    file.close();
}

void M5Display::drawPngUrl(const char *url, uint16_t x, uint16_t y,
                           uint16_t maxWidth, uint16_t maxHeight, uint16_t offX,
                           uint16_t offY, double scale,
                           uint8_t alphaThreshold) {
    HTTPClient http;

    if (WiFi.status() != WL_CONNECTED) {
        log_e("Not connected");
        return;
    }

    http.begin(url);

    int httpCode = http.GET();
    if (httpCode != HTTP_CODE_OK) {
        log_e("HTTP ERROR: %d\n", httpCode);
        http.end();
        return;
    }

    WiFiClient *stream = http.getStreamPtr();

    pngle_t *pngle = pngle_new();

    png_file_decoder_t png;

    if (!maxWidth) {
        maxWidth = width() - x;
    }
    if (!maxHeight) {
        maxHeight = height() - y;
    }

    png.x              = x;
    png.y              = y;
    png.maxWidth       = maxWidth;
    png.maxHeight      = maxHeight;
    png.offX           = offX;
    png.offY           = offY;
    png.scale          = scale;
    png.alphaThreshold = alphaThreshold;
    png.tft            = this;

    pngle_set_user_data(pngle, &png);
    pngle_set_draw_callback(pngle, pngle_draw_callback);

    // Feed data to pngle
    uint8_t buf[1024];
    int remain = 0;
    int len;
    while (http.connected()) {
        size_t size = stream->available();
        if (!size) {
            delay(1);
            continue;
        }

        if (size > sizeof(buf) - remain) size = sizeof(buf) - remain;
        if ((len = stream->readBytes(buf + remain, size)) > 0) {
            int fed = pngle_feed(pngle, buf, remain + len);
            if (fed < 0) {
                log_e("[pngle error] %s", pngle_error(pngle));
                break;
            }

            remain = remain + len - fed;
            if (remain > 0) memmove(buf, buf + fed, remain);
        }
    }

    pngle_destroy(pngle);
    http.end();
}

// Saves and restores font properties, datum, cursor, colors

void M5Display::pushState() {
    DisplayState s;
    s.textfont    = textfont;
    s.textsize    = textsize;
    s.textcolor   = textcolor;
    s.textbgcolor = textbgcolor;
    s.cursor_x    = cursor_x;
    s.cursor_y    = cursor_y;
    s.padX        = padX;
    s.gfxFont     = gfxFont;
    _displayStateStack.push_back(s);
}

void M5Display::popState() {
    if (_displayStateStack.empty()) return;
    DisplayState s = _displayStateStack.back();
    _displayStateStack.pop_back();
    textfont    = s.textfont;
    textsize    = s.textsize;
    textcolor   = s.textcolor;
    textbgcolor = s.textbgcolor;
    cursor_x    = s.cursor_x;
    cursor_y    = s.cursor_y;
    padX        = s.padX;
    if (s.gfxFont && s.gfxFont != gfxFont) setFreeFont(s.gfxFont);
}

#ifdef M5Stack_M5Core2

#ifdef TFT_eSPI_TOUCH_EMULATION

// Emulates the native (resistive) TFT_eSPI touch interface using M5.Touch

uint8_t M5Display::getTouchRaw(uint16_t *x, uint16_t *y) {
    return getTouch(x, y);
}

uint16_t M5Display::getTouchRawZ(void) {
    return (TOUCH->ispressed()) ? 1000 : 0;
}

void M5Display::convertRawXY(uint16_t *x, uint16_t *y) {
    return;
}

uint8_t M5Display::getTouch(uint16_t *x, uint16_t *y,
                            uint16_t threshold /* = 600 */) {
    TOUCH->read();
    if (TOUCH->points) {
        *x = TOUCH->point[0].x;
        *y = TOUCH->point[0].y;
        return true;
    }
    return false;
}

void M5Display::calibrateTouch(uint16_t *data, uint32_t color_fg,
                               uint32_t color_bg, uint8_t size) {
    return;
}

void M5Display::setTouch(uint16_t *data) {
    return;
}

#endif /* TFT_eSPI_TOUCH_EMULATION */

#endif /* M5Stack_M5Core2 */
