/*******************************************************************************
 * GIFDEC Wrapper Class
 *
 * Rewrite from: https://github.com/BasementCat/arduino-tft-gif
 ******************************************************************************/
#pragma once

// #define GIF_HANDLE_TRANSPARENT

/* Wio Terminal */
#if defined(ARDUINO_ARCH_SAMD) && defined(SEEED_GROVE_UI_WIRELESS)
#include <Seeed_FS.h>
#elif defined(ESP32) || defined(ESP8266)
#include <FS.h>
#else
#include <SD.h>
#endif

#include <sys/types.h>

#ifndef MIN
#define MIN(A, B) ((A) < (B) ? (A) : (B))
#endif

#ifndef MAX
#define MAX(A, B) ((A) > (B) ? (A) : (B))
#endif

#define GIF_BUF_SIZE 1024

typedef struct gd_Palette
{
    int16_t len;
    uint16_t colors[256];
} gd_Palette;

typedef struct gd_GCE
{
    uint16_t delay;
    uint8_t tindex;
    uint8_t disposal;
    uint8_t input;
    uint8_t transparency;
} gd_GCE;

typedef struct gd_Entry
{
    int32_t len;
    uint16_t prefix;
    uint8_t suffix;
} gd_Entry;

typedef struct gd_Table
{
    int16_t bulk;
    int16_t nentries;
    gd_Entry *entries;
} gd_Table;

typedef struct gd_GIF
{
    File *fd;
    off_t anim_start;
    uint16_t width, height;
    uint16_t depth;
    uint16_t loop_count;
    gd_GCE gce;
    gd_Palette *palette;
    gd_Palette lct, gct;
    void (*plain_text)(
        struct gd_GIF *gif, uint16_t tx, uint16_t ty,
        uint16_t tw, uint16_t th, uint8_t cw, uint8_t ch,
        uint8_t fg, uint8_t bg);
    void (*comment)(struct gd_GIF *gif);
    void (*application)(struct gd_GIF *gif, char id[8], char auth[3]);
    uint16_t fx, fy, fw, fh;
    uint8_t bgindex;
    gd_Table *table;
    bool processed_first_frame;
} gd_GIF;

class GifClass
{
public:
    gd_GIF *gd_open_gif(File *fd)
    {
        uint8_t sigver[3];
        uint16_t width, height, depth;
        uint8_t fdsz, bgidx, aspect;
        int16_t gct_sz;
        gd_GIF *gif;

        // init global variables
        gif_buf_last_idx = GIF_BUF_SIZE;
        gif_buf_idx = gif_buf_last_idx; // no buffer yet
        file_pos = 0;

        /* Header */
        gif_buf_read(fd, sigver, 3);
        if (memcmp(sigver, "GIF", 3) != 0)
        {
            Serial.println(F("invalid signature"));
            return NULL;
        }
        /* Version */
        gif_buf_read(fd, sigver, 3);
        if (memcmp(sigver, "89a", 3) != 0)
        {
            Serial.println(F("invalid version"));
            return NULL;
        }
        /* Width x Height */
        width = gif_buf_read16(fd);
        height = gif_buf_read16(fd);
        /* FDSZ */
        gif_buf_read(fd, &fdsz, 1);
        /* Presence of GCT */
        if (!(fdsz & 0x80))
        {
            Serial.println(F("no global color table"));
            return NULL;
        }
        /* Color Space's Depth */
        depth = ((fdsz >> 4) & 7) + 1;
        /* Ignore Sort Flag. */
        /* GCT Size */
        gct_sz = 1 << ((fdsz & 0x07) + 1);
        /* Background Color Index */
        gif_buf_read(fd, &bgidx, 1);
        /* Aspect Ratio */
        gif_buf_read(fd, &aspect, 1);
        /* Create gd_GIF Structure. */
        gif = (gd_GIF *)calloc(1, sizeof(*gif));
        gif->fd = fd;
        gif->width = width;
        gif->height = height;
        gif->depth = depth;
        /* Read GCT */
        read_palette(fd, &gif->gct, gct_sz);
        gif->palette = &gif->gct;
        gif->bgindex = bgidx;
        gif->anim_start = file_pos; // fd->position();
        gif->table = new_table();
        gif->processed_first_frame = false;
        return gif;
    }

    /* Return 1 if got a frame; 0 if got GIF trailer; -1 if error. */
    int32_t gd_get_frame(gd_GIF *gif, uint8_t *frame)
    {
        char sep;

        while (1)
        {
            gif_buf_read(gif->fd, (uint8_t *)&sep, 1);
            if (sep == 0)
            {
                gif_buf_read(gif->fd, (uint8_t *)&sep, 1);
            }
            if (sep == ',')
            {
                break;
            }
            if (sep == ';')
            {
                return 0;
            }
            if (sep == '!')
            {
                read_ext(gif);
            }
            else
            {
                Serial.print(F("Read sep: ["));
                Serial.print(sep);
                Serial.println(F("].\n"));
                return -1;
            }
        }
        // Serial.println("Do read image");
        if (read_image(gif, frame) == -1)
            return -1;
        return 1;
    }

    void gd_rewind(gd_GIF *gif)
    {
#if defined(ESP32) || defined(ESP8266)
        gif->fd->seek(gif->anim_start, SeekSet);
#else
        gif->fd->seek(gif->anim_start);
#endif
        file_pos = gif->anim_start;
        gif_buf_idx = gif_buf_last_idx; // reset buffer
    }

    void gd_close_gif(gd_GIF *gif)
    {
        gif->fd->close();
        free(gif->table);
        free(gif);
    }

private:
    bool gif_buf_seek(File *fd, int16_t len)
    {
        if (len > (gif_buf_last_idx - gif_buf_idx))
        {
#if defined(ESP32) || defined(ESP8266)
            // fd->seek(len - (gif_buf_last_idx - gif_buf_idx), SeekCur);
            fd->seek(file_pos + len - (gif_buf_last_idx - gif_buf_idx), SeekSet);
#else
            fd->seek(file_pos + len - (gif_buf_last_idx - gif_buf_idx));
#endif

            gif_buf_idx = gif_buf_last_idx;
        }
        else
        {
            gif_buf_idx += len;
        }
        file_pos += len;

        return true;
    }

    int16_t gif_buf_read(File *fd, uint8_t *dest, int16_t len)
    {
        while (len--)
        {
            if (gif_buf_idx == gif_buf_last_idx)
            {
                gif_buf_last_idx = fd->read(gif_buf, GIF_BUF_SIZE);
                gif_buf_idx = 0;
            }

            file_pos++;
            *(dest++) = gif_buf[gif_buf_idx++];
        }
        return len;
    }

    uint8_t gif_buf_read(File *fd)
    {
        if (gif_buf_idx == gif_buf_last_idx)
        {
            gif_buf_last_idx = fd->read(gif_buf, GIF_BUF_SIZE);
            gif_buf_idx = 0;
        }

        file_pos++;
        return gif_buf[gif_buf_idx++];
    }

    uint16_t gif_buf_read16(File *fd)
    {
        return gif_buf_read(fd) + (((uint16_t)gif_buf_read(fd)) << 8);
    }

    void read_palette(File *fd, gd_Palette *dest, int16_t num_colors)
    {
        uint8_t r, g, b;
        dest->len = num_colors;
        for (int16_t i = 0; i < num_colors; i++)
        {
            r = gif_buf_read(fd);
            g = gif_buf_read(fd);
            b = gif_buf_read(fd);
            dest->colors[i] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3);
        }
    }

    void discard_sub_blocks(gd_GIF *gif)
    {
        uint8_t len;

        do
        {
            gif_buf_read(gif->fd, &len, 1);
            gif_buf_seek(gif->fd, len);
        } while (len);
    }

    void read_plain_text_ext(gd_GIF *gif)
    {
        if (gif->plain_text)
        {
            uint16_t tx, ty, tw, th;
            uint8_t cw, ch, fg, bg;
            gif_buf_seek(gif->fd, 1); /* block size = 12 */
            tx = gif_buf_read16(gif->fd);
            ty = gif_buf_read16(gif->fd);
            tw = gif_buf_read16(gif->fd);
            th = gif_buf_read16(gif->fd);
            cw = gif_buf_read(gif->fd);
            ch = gif_buf_read(gif->fd);
            fg = gif_buf_read(gif->fd);
            bg = gif_buf_read(gif->fd);
            gif->plain_text(gif, tx, ty, tw, th, cw, ch, fg, bg);
        }
        else
        {
            /* Discard plain text metadata. */
            gif_buf_seek(gif->fd, 13);
        }
        /* Discard plain text sub-blocks. */
        discard_sub_blocks(gif);
    }

    void read_graphic_control_ext(gd_GIF *gif)
    {
        uint8_t rdit;

        /* Discard block size (always 0x04). */
        gif_buf_seek(gif->fd, 1);
        gif_buf_read(gif->fd, &rdit, 1);
        gif->gce.disposal = (rdit >> 2) & 3;
        gif->gce.input = rdit & 2;
        gif->gce.transparency = rdit & 1;
        gif->gce.delay = gif_buf_read16(gif->fd);
        gif_buf_read(gif->fd, &gif->gce.tindex, 1);
        /* Skip block terminator. */
        gif_buf_seek(gif->fd, 1);
    }

    void read_comment_ext(gd_GIF *gif)
    {
        if (gif->comment)
        {
            gif->comment(gif);
        }
        /* Discard comment sub-blocks. */
        discard_sub_blocks(gif);
    }

    void read_application_ext(gd_GIF *gif)
    {
        char app_id[8];
        char app_auth_code[3];

        /* Discard block size (always 0x0B). */
        gif_buf_seek(gif->fd, 1);
        /* Application Identifier. */
        gif_buf_read(gif->fd, (uint8_t *)app_id, 8);
        /* Application Authentication Code. */
        gif_buf_read(gif->fd, (uint8_t *)app_auth_code, 3);
        if (!strncmp(app_id, "NETSCAPE", sizeof(app_id)))
        {
            /* Discard block size (0x03) and constant byte (0x01). */
            gif_buf_seek(gif->fd, 2);
            gif->loop_count = gif_buf_read16(gif->fd);
            /* Skip block terminator. */
            gif_buf_seek(gif->fd, 1);
        }
        else if (gif->application)
        {
            gif->application(gif, app_id, app_auth_code);
            discard_sub_blocks(gif);
        }
        else
        {
            discard_sub_blocks(gif);
        }
    }

    void read_ext(gd_GIF *gif)
    {
        uint8_t label;

        gif_buf_read(gif->fd, &label, 1);
        switch (label)
        {
        case 0x01:
            read_plain_text_ext(gif);
            break;
        case 0xF9:
            read_graphic_control_ext(gif);
            break;
        case 0xFE:
            read_comment_ext(gif);
            break;
        case 0xFF:
            read_application_ext(gif);
            break;
        default:
            Serial.print("unknown extension: ");
            Serial.println(label, HEX);
        }
    }

    gd_Table *new_table()
    {
        // uint16_t key;
        // int16_t init_bulk = MAX(1 << (key_size + 1), 0x100);
        // Table *table = (Table*) malloc(sizeof(*table) + sizeof(Entry) * init_bulk);
        // if (table) {
        //     table->bulk = init_bulk;
        //     table->nentries = (1 << key_size) + 2;
        //     table->entries = (Entry *) &table[1];
        //     for (key = 0; key < (1 << key_size); key++)
        //         table->entries[key] = (Entry) {1, 0xFFF, key};
        // }
        // return table;
        int32_t s = sizeof(gd_Table) + (sizeof(gd_Entry) * 4096);
        gd_Table *table = (gd_Table *)malloc(s);
        if (table)
        {
            Serial.print(F("new_table() malloc: "));
            Serial.println(s);
        }
        else
        {
            Serial.print(F("new_table() malloc failed: "));
            Serial.println(s);
        }
        table->entries = (gd_Entry *)&table[1];
        return table;
    }

    void reset_table(gd_Table *table, uint16_t key_size)
    {
        table->nentries = (1 << key_size) + 2;
        for (uint16_t key = 0; key < (1 << key_size); key++)
        {
            table->entries[key] = (gd_Entry){1, 0xFFF, (uint8_t)key};
        }
    }

    /* Add table entry. Return value:
     *  0 on success
     *  +1 if key size must be incremented after this addition
     *  -1 if could not realloc table */
    int32_t add_entry(gd_Table *table, int32_t len, uint16_t prefix, uint8_t suffix)
    {
        // Table *table = *tablep;
        // if (table->nentries == table->bulk) {
        //     table->bulk *= 2;
        //     table = (Table*) realloc(table, sizeof(*table) + sizeof(Entry) * table->bulk);
        //     if (!table) return -1;
        //     table->entries = (Entry *) &table[1];
        //     *tablep = table;
        // }
        table->entries[table->nentries] = (gd_Entry){len, prefix, suffix};
        table->nentries++;
        if ((table->nentries & (table->nentries - 1)) == 0)
            return 1;
        return 0;
    }

    uint16_t get_key(gd_GIF *gif, uint16_t key_size, uint8_t *sub_len, uint8_t *shift, uint8_t *byte)
    {
        int16_t bits_read;
        int16_t rpad;
        int16_t frag_size;
        uint16_t key;

        key = 0;
        for (bits_read = 0; bits_read < key_size; bits_read += frag_size)
        {
            rpad = (*shift + bits_read) % 8;
            if (rpad == 0)
            {
                /* Update byte. */
                if (*sub_len == 0)
                    gif_buf_read(gif->fd, sub_len, 1); /* Must be nonzero! */
                gif_buf_read(gif->fd, byte, 1);
                (*sub_len)--;
            }
            frag_size = MIN(key_size - bits_read, 8 - rpad);
            key |= ((uint16_t)((*byte) >> rpad)) << bits_read;
        }
        /* Clear extra bits to the left. */
        key &= (1 << key_size) - 1;
        *shift = (*shift + key_size) % 8;
        return key;
    }

    /* Compute output index of y-th input line, in frame of height h. */
    int16_t interlaced_line_index(int16_t h, int16_t y)
    {
        int16_t p; /* number of lines in current pass */

        p = (h - 1) / 8 + 1;
        if (y < p) /* pass 1 */
            return y * 8;
        y -= p;
        p = (h - 5) / 8 + 1;
        if (y < p) /* pass 2 */
            return y * 8 + 4;
        y -= p;
        p = (h - 3) / 4 + 1;
        if (y < p) /* pass 3 */
            return y * 4 + 2;
        y -= p;
        /* pass 4 */
        return y * 2 + 1;
    }

    /* Decompress image pixels.
     * Return 0 on success or -1 on out-of-memory (w.r.t. LZW code table). */
    int8_t read_image_data(gd_GIF *gif, int16_t interlace, uint8_t *frame)
    {
        uint8_t sub_len, shift, byte, table_is_full = 0;
        uint16_t init_key_size, key_size;
        int32_t frm_off, str_len = 0, p, x, y;
        uint16_t key, clear, stop;
        int32_t ret;
        gd_Entry entry = {0, 0, 0};

        // Serial.println("Read key size");
        gif_buf_read(gif->fd, &byte, 1);
        key_size = (uint16_t)byte;
        // Serial.println("Set pos, discard sub blocks");
        // start = gif->fd->position();
        // discard_sub_blocks(gif);
        // end = gif->fd->position();
        // gif_buf_seek(gif->fd, start, SeekSet);
        clear = 1 << key_size;
        stop = clear + 1;
        // Serial.println("New LZW table");
        // table = new_table(key_size);
        reset_table(gif->table, key_size);
        key_size++;
        init_key_size = key_size;
        sub_len = shift = 0;
        // Serial.println("Get init key");
        key = get_key(gif, key_size, &sub_len, &shift, &byte); /* clear code */
        frm_off = 0;
        ret = 0;
        while (1)
        {
            if (key == clear)
            {
                // Serial.println("Clear key, reset nentries");
                key_size = init_key_size;
                gif->table->nentries = (1 << (key_size - 1)) + 2;
                table_is_full = 0;
            }
            else if (!table_is_full)
            {
                // Serial.println("Add entry to table");
                ret = add_entry(gif->table, str_len + 1, key, entry.suffix);
                // if (ret == -1) {
                //     // Serial.println("Table entry add failure");
                //     free(table);
                //     return -1;
                // }
                if (gif->table->nentries == 0x1000)
                {
                    // Serial.println("Table is full");
                    ret = 0;
                    table_is_full = 1;
                }
            }
            // Serial.println("Get key");
            key = get_key(gif, key_size, &sub_len, &shift, &byte);
            if (key == clear)
                continue;
            if (key == stop)
                break;
            if (ret == 1)
                key_size++;
            entry = gif->table->entries[key];
            str_len = entry.len;
            uint8_t tindex = gif->gce.tindex;
            // Serial.println("Interpret key");
            while (1)
            {
                p = frm_off + entry.len - 1;
                x = p % gif->fw;
                y = p / gif->fw;
                if (interlace)
                {
                    y = interlaced_line_index((int16_t)gif->fh, y);
                }
#ifdef GIF_HANDLE_TRANSPARENT
                if ((!gif->processed_first_frame) || (tindex != entry.suffix))
#endif
                {
                    frame[(gif->fy + y) * gif->width + gif->fx + x] = entry.suffix;
                }
                if (entry.prefix == 0xFFF)
                    break;
                else
                    entry = gif->table->entries[entry.prefix];
            }
            frm_off += str_len;
            if (key < gif->table->nentries - 1 && !table_is_full)
                gif->table->entries[gif->table->nentries - 1].suffix = entry.suffix;
        }
        // Serial.println("Done w/ img data, free table and seek to end");
        // free(table);
        gif_buf_read(gif->fd, &sub_len, 1); /* Must be zero! */
        // gif_buf_seek(gif->fd, end, SeekSet);

        gif->processed_first_frame = true;

        return 0;
    }

    /* Read image.
     * Return 0 on success or -1 on out-of-memory (w.r.t. LZW code table). */
    int8_t read_image(gd_GIF *gif, uint8_t *frame)
    {
        uint8_t fisrz;
        int16_t interlace;

        /* Image Descriptor. */
        // Serial.println("Read image descriptor");
        gif->fx = gif_buf_read16(gif->fd);
        gif->fy = gif_buf_read16(gif->fd);
        gif->fw = gif_buf_read16(gif->fd);
        gif->fh = gif_buf_read16(gif->fd);
        // Serial.println("Read fisrz?");
        gif_buf_read(gif->fd, &fisrz, 1);
        interlace = fisrz & 0x40;
        /* Ignore Sort Flag. */
        /* Local Color Table? */
        if (fisrz & 0x80)
        {
            /* Read LCT */
            // Serial.println("Read LCT");
            read_palette(gif->fd, &gif->lct, 1 << ((fisrz & 0x07) + 1));
            gif->palette = &gif->lct;
        }
        else
        {
            gif->palette = &gif->gct;
        }
        /* Image Data. */
        // Serial.println("Read image data");
        return read_image_data(gif, interlace, frame);
    }

    int16_t gif_buf_last_idx, gif_buf_idx, file_pos;
    uint8_t gif_buf[GIF_BUF_SIZE];
};
