#ifndef GifPlayer_H
#define GifPlayer_H

class GifPlayer
{
public:
#define ERROR_NONE 0
#define ERROR_FILEOPEN 1
#define ERROR_FILENOTGIF 2
#define ERROR_BADGIFFORMAT 3
#define ERROR_UNKNOWNCONTROLEXT 4
#define ERROR_FINISHED 5
private:
  bool needNewFrame;
  long lastFrameTime;
  bool firstFrameDone;
  int newframeDelay;
  int lastFrame[8 * 8]; 
  bool lastFrameDrawn = false; 
  unsigned long nextFrameTime = 0;
#define GIFHDRTAGNORM "GIF87a" 
#define GIFHDRTAGNORM1 "GIF89a"
#define GIFHDRSIZE 6
  FastLED_NeoMatrix *mtx;
#define COLORTBLFLAG 0x80
#define INTERLACEFLAG 0x40
#define TRANSPARENTFLAG 0x01
#define NO_TRANSPARENT_INDEX -1
#define DISPOSAL_NONE 0
#define DISPOSAL_LEAVE 1
#define DISPOSAL_BACKGROUND 2
#define DISPOSAL_RESTORE 3
  typedef struct
  {
    byte Red;
    byte Green;
    byte Blue;
  } _RGB;
  int lsdWidth;
  int lsdHeight;
  int lsdPackedField;
  int lsdAspectRatio;
  int lsdBackgroundIndex;
  int offsetX;
  int offsetY;
  int tbiImageX;
  int tbiImageY;
  int tbiWidth;
  int tbiHeight;
  int tbiPackedBits;
  boolean tbiInterlaced;

public:
  int frameDelay;
  int transparentColorIndex;
  int prevBackgroundIndex;
  int prevDisposalMethod;
  int disposalMethod;
  int lzwCodeSize;
  boolean keyFrame;
  int rectX;
  int rectY;
  int rectWidth;
  int rectHeight;
  int colorCount;
  _RGB gifPalette[256];
  byte lzwImageData[1280];
  char tempBuffer[260];
  File file;
  byte imageData[8 * 8];
  byte imageDataBU[8 * 8];

  void backUpStream(int n)
  {
    file.seek(file.position() - n, SeekSet);
  }

  int readByte()
  {
    int b = file.read();
    return b;
  }

  int readWord()
  {
    int b0 = readByte();
    int b1 = readByte();
    return (b1 << 8) | b0;
  }

  int readIntoBuffer(void *buffer, int numberOfBytes)
  {
    int result = file.read((uint8_t *)buffer, numberOfBytes);
    return result;
  }

  void fillImageDataRect(byte colorIndex, int x, int y, int width, int height)
  {
    int yOffset;
    for (int yy = y; yy < height + y; yy++)
    {
      yOffset = yy * 8;
      for (int xx = x; xx < width + x; xx++)
      {
        imageData[yOffset + xx] = colorIndex;
      }
    }
  }

  void fillImageData(byte colorIndex)
  {
    memset(imageData, colorIndex, sizeof(imageData));
  }

  void copyImageDataRect(byte *src, byte *dst, int x, int y, int width, int height)
  {

    int yOffset, offset;

    for (int yy = y; yy < height + y; yy++)
    {
      yOffset = yy * 8;
      for (int xx = x; xx < width + x; xx++)
      {
        offset = yOffset + xx;
        dst[offset] = src[offset];
      }
    }
  }

  void parsePlainTextExtension()
  {
    byte len = readByte();
    readIntoBuffer(tempBuffer, len);
    len = readByte();
    while (len != 0)
    {
      readIntoBuffer(tempBuffer, len);
      len = readByte();
    }
  }

  void parseGraphicControlExtension()
  {
    readByte();
    int packedBits = readByte();
    frameDelay = readWord();
    transparentColorIndex = readByte();

    if ((packedBits & TRANSPARENTFLAG) == 0)
    {
      // Indicate no transparent index
      transparentColorIndex = NO_TRANSPARENT_INDEX;
    }
    disposalMethod = (packedBits >> 2) & 7;
    if (disposalMethod > 3)
    {
      disposalMethod = 0;
    }

    readByte(); // Toss block end
  }

  void parseApplicationExtension()
  {
    memset(tempBuffer, 0, sizeof(tempBuffer));
    byte len = readByte();
    readIntoBuffer(tempBuffer, len);
    len = readByte();
    while (len != 0)
    {
      readIntoBuffer(tempBuffer, len);
      len = readByte();
    }
  }

  void parseCommentExtension()
  {
    byte len = readByte();
    while (len != 0)
    {
      memset(tempBuffer, 0, sizeof(tempBuffer));
      readIntoBuffer(tempBuffer, len);
      len = readByte();
    }
  }

  int parseGIFFileTerminator()
  {
    byte b = readByte();
    if (b != 0x3B)
    {
      return ERROR_BADGIFFORMAT;
    }
    else
    {
      return ERROR_NONE;
    }
  }

  unsigned long parseTableBasedImage()
  {
    tbiImageX = readWord();
    tbiImageY = readWord();
    tbiWidth = readWord();
    tbiHeight = readWord();
    tbiPackedBits = readByte();
    tbiInterlaced = ((tbiPackedBits & INTERLACEFLAG) != 0);
    boolean localColorTable = ((tbiPackedBits & COLORTBLFLAG) != 0);
    if (localColorTable)
    {
      int colorBits = ((tbiPackedBits & 7) + 1);
      colorCount = 1 << colorBits;
      int colorTableBytes = sizeof(_RGB) * colorCount;
      readIntoBuffer(gifPalette, colorTableBytes);
    }

    if (keyFrame)
    {
      if (transparentColorIndex == NO_TRANSPARENT_INDEX)
      {
        fillImageData(lsdBackgroundIndex);
      }
      else
      {
        fillImageData(transparentColorIndex);
      }
      keyFrame = false;

      rectX = 0;
      rectY = 0;
      rectWidth = 8;
      rectHeight = 8;
    }

    if ((prevDisposalMethod != DISPOSAL_NONE) && (prevDisposalMethod != DISPOSAL_LEAVE))
    {
      // mtx->clear();
    }

    if (prevDisposalMethod == DISPOSAL_BACKGROUND)
    {
      fillImageDataRect(prevBackgroundIndex, rectX, rectY, rectWidth, rectHeight);
    }
    else if (prevDisposalMethod == DISPOSAL_RESTORE)
    {
      copyImageDataRect(imageDataBU, imageData, rectX, rectY, rectWidth, rectHeight);
    }
    prevDisposalMethod = disposalMethod;
    if (disposalMethod != DISPOSAL_NONE)
    {
      rectX = tbiImageX;
      rectY = tbiImageY;
      rectWidth = tbiWidth;
      rectHeight = tbiHeight;
      if (disposalMethod == DISPOSAL_BACKGROUND)
      {
        if (transparentColorIndex != NO_TRANSPARENT_INDEX)
        {
          prevBackgroundIndex = transparentColorIndex;
        }
        else
        {
          prevBackgroundIndex = lsdBackgroundIndex;
        }
      }
      else if (disposalMethod == DISPOSAL_RESTORE)
      {
        copyImageDataRect(imageData, imageDataBU, rectX, rectY, rectWidth, rectHeight);
      }
    }
    lzwCodeSize = readByte();
    int offset = 0;
    int dataBlockSize = readByte();
    while (dataBlockSize != 0)
    {
      backUpStream(1);
      dataBlockSize++;
      if (offset + dataBlockSize <= (int)sizeof(lzwImageData))
      {
        readIntoBuffer(lzwImageData + offset, dataBlockSize);
      }
      else
      {
        int i;
        for (i = 0; i < dataBlockSize; i++)
          file.read();
      }

      offset += dataBlockSize;
      dataBlockSize = readByte();
    }
    lzw_decode_init(lzwCodeSize, lzwImageData);
    decompressAndDisplayFrame();
    transparentColorIndex = NO_TRANSPARENT_INDEX;
    disposalMethod = DISPOSAL_NONE;
    if (frameDelay < 1)
    {
      frameDelay = 1;
    }
    newframeDelay = frameDelay * 10;
    return frameDelay * 10;
  }

#define LZW_MAXBITS 10
#define LZW_SIZTABLE (1 << LZW_MAXBITS)
  unsigned int mask[17] = {
      0x0000, 0x0001, 0x0003, 0x0007,
      0x000F, 0x001F, 0x003F, 0x007F,
      0x00FF, 0x01FF, 0x03FF, 0x07FF,
      0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF,
      0xFFFF};

  byte *pbuf;
  int bbits;
  int bbuf;
  int cursize; // The current code size
  int curmask;
  int codesize;
  int clear_code;
  int end_code;
  int newcodes; // First available code
  int top_slot; // Highest code for current size
  int extra_slot;
  int slot; // Last read code
  int fc, oc;
  int bs; // Current buffer size for GIF
  byte *sp;
  byte stack[LZW_SIZTABLE];
  byte suffix[LZW_SIZTABLE];
  unsigned int prefix[LZW_SIZTABLE];

  void lzw_decode_init(int csize, byte *buf)
  {
    pbuf = buf;
    bbuf = 0;
    bbits = 0;
    bs = 0;
    codesize = csize;
    cursize = codesize + 1;
    curmask = mask[cursize];
    top_slot = 1 << cursize;
    clear_code = 1 << codesize;
    end_code = clear_code + 1;
    slot = newcodes = clear_code + 2;
    oc = fc = -1;
    sp = stack;
  }

  int lzw_get_code()
  {
    while (bbits < cursize)
    {
      if (!bs)
      {
        bs = *pbuf++;
      }
      bbuf |= (*pbuf++) << bbits;
      bbits += 8;
      bs--;
    }
    int c = bbuf;
    bbuf >>= cursize;
    bbits -= cursize;
    return c & curmask;
  }

  int lzw_decode(byte *buf, int len)
  {
    int l, c, code;

    if (end_code < 0)
    {
      return 0;
    }
    l = len;

    for (;;)
    {
      while (sp > stack)
      {
        *buf++ = *(--sp);
        if ((--l) == 0)
        {
          goto the_end;
        }
      }
      c = lzw_get_code();
      if (c == end_code)
      {
        break;
      }
      else if (c == clear_code)
      {
        cursize = codesize + 1;
        curmask = mask[cursize];
        slot = newcodes;
        top_slot = 1 << cursize;
        fc = oc = -1;
      }
      else
      {

        code = c;
        if ((code == slot) && (fc >= 0))
        {
          *sp++ = fc;
          code = oc;
        }
        else if (code >= slot)
        {
          break;
        }
        while (code >= newcodes)
        {
          *sp++ = suffix[code];
          code = prefix[code];
        }
        *sp++ = code;
        if ((slot < top_slot) && (oc >= 0))
        {
          suffix[slot] = code;
          prefix[slot++] = oc;
        }
        fc = code;
        oc = c;
        if (slot >= top_slot)
        {
          if (cursize < LZW_MAXBITS)
          {
            top_slot <<= 1;
            curmask = mask[++cursize];
          }
          else
          {
          }
        }
      }
    }
    end_code = -1;
  the_end:
    return len - l;
  }

  void redrawLastFrame()
  {
    if (needNewFrame)
      return;
    CRGB color;
    int yOffset, pixel;
    for (int y = tbiImageY; y < tbiHeight + tbiImageY; y++)
    {
      yOffset = y * 8;
      for (int x = tbiImageX; x < tbiWidth + tbiImageX; x++)
      {
        pixel = lastFrame[yOffset + x];
        if (pixel == -99)
        {
          mtx->drawPixel(x + offsetX, y + offsetY, mtx->Color(0, 0, 0));
          continue;
        }
        color.red = gifPalette[pixel].Red;
        color.green = gifPalette[pixel].Green;
        color.blue = gifPalette[pixel].Blue;
        mtx->drawPixel(x + offsetX, y + offsetY, color);
      }
    }
  }

  void decompressAndDisplayFrame()
  {
    CRGB color;
    if (tbiInterlaced)
    {
      for (int line = tbiImageY + 0; line < tbiHeight + tbiImageY; line += 8)
      {
        lzw_decode(imageData + (line * 8) + tbiImageX, tbiWidth);
      }
      for (int line = tbiImageY + 4; line < tbiHeight + tbiImageY; line += 8)
      {
        lzw_decode(imageData + (line * 8) + tbiImageX, tbiWidth);
      }
      for (int line = tbiImageY + 2; line < tbiHeight + tbiImageY; line += 4)
      {
        lzw_decode(imageData + (line * 8) + tbiImageX, tbiWidth);
      }
      for (int line = tbiImageY + 1; line < tbiHeight + tbiImageY; line += 2)
      {
        lzw_decode(imageData + (line * 8) + tbiImageX, tbiWidth);
      }
    }
    else
    {
      for (int line = tbiImageY; line < tbiHeight + tbiImageY; line++)
      {
        lzw_decode(imageData + (line * 8) + tbiImageX, tbiWidth);
      }
    }

    // Ersetze alle transparenten Pixel durch schwarze Pixel
    for (int i = 0; i < tbiWidth * tbiHeight; i++)
    {
      if (imageData[i] == transparentColorIndex)
      {
        imageData[i] = -99; // Indexwert für Schwarz
      }
    }

    // Zeichne das Bild auf die Matrix
    int yOffset, pixel;
    for (int y = tbiImageY; y < tbiHeight + tbiImageY; y++)
    {
      yOffset = y * 8;
      for (int x = tbiImageX; x < tbiWidth + tbiImageX; x++)
      {
        pixel = imageData[yOffset + x];
        if (pixel == -99)
        {
          mtx->drawPixel(x + offsetX, y + offsetY, mtx->Color(0, 0, 0));
          continue;
        }

        lastFrame[yOffset + x] = pixel;
        color.red = gifPalette[pixel].Red;
        color.green = gifPalette[pixel].Green;
        color.blue = gifPalette[pixel].Blue;
        mtx->drawPixel(x + offsetX, y + offsetY, color);
      }
    }
    needNewFrame = false;
  }

public:
  void setMatrix(FastLED_NeoMatrix *matrix)
  {
    mtx = matrix;
  }
  void setFile(File imageFile)
  {
    if (imageFile.name() == file.name())
      return;

    needNewFrame = true;
    file = imageFile;
    memset(lastFrame, 0, sizeof(lastFrame));
    memset(gifPalette, 0, sizeof(gifPalette));
    memset(lzwImageData, 0, sizeof(lzwImageData));
    memset(imageData, 0, sizeof(imageData));
    memset(imageDataBU, 0, sizeof(imageDataBU));
    memset(stack, 0, sizeof(stack));
    memset(suffix, 0, sizeof(suffix));
    memset(prefix, 0, sizeof(prefix));
    parseGifHeader();
    parseLogicalScreenDescriptor();
    parseGlobalColorTable();
    drawFrame(offsetX, offsetY);
  }

  boolean parseGifHeader()
  {
    char buffer[10];
    readIntoBuffer(buffer, GIFHDRSIZE);
    if ((strncmp(buffer, GIFHDRTAGNORM, GIFHDRSIZE) != 0) &&
        (strncmp(buffer, GIFHDRTAGNORM1, GIFHDRSIZE) != 0))
    {
      return false;
    }
    else
    {
      return true;
    }
  }

  void parseLogicalScreenDescriptor()
  {
    lsdWidth = readWord();
    lsdHeight = readWord();
    lsdPackedField = readByte();
    lsdBackgroundIndex = readByte();
    lsdAspectRatio = readByte();
  }

  void parseGlobalColorTable()
  {
    if (lsdPackedField & COLORTBLFLAG)
    {
      colorCount = 1 << ((lsdPackedField & 7) + 1);
      int colorTableBytes = sizeof(_RGB) * colorCount;
      readIntoBuffer(gifPalette, colorTableBytes);
    }
  }

  unsigned long drawFrame(int x, int y)
  {
    if (!file)
      return 0;

    unsigned long now = millis();

    if (now - lastFrameTime < newframeDelay)
    {
      redrawLastFrame();
      return 0;
    }

    lastFrameTime = now;
    lastFrameDrawn = false;

    offsetX = x;
    offsetY = y;
    boolean done = false;
    while (!done)
    {
      byte b = readByte();
      if (b == 0x2c)
      {
        unsigned int fdelay = parseTableBasedImage();
        return fdelay;
      }
      else if (b == 0x21)
      {
        b = readByte();
        switch (b)
        {
        case 0x01:
          parsePlainTextExtension();
          break;
        case 0xf9:
          parseGraphicControlExtension();
          break;
        case 0xfe:
          parseCommentExtension();
          break;
        case 0xff:
          parseApplicationExtension();
          break;
        default:
          return ERROR_UNKNOWNCONTROLEXT;
        }
      }
      else
      {
        done = true;
        backUpStream(1);
        file.seek(0);
        parseGifHeader();
        parseLogicalScreenDescriptor();
        parseGlobalColorTable();
        drawFrame(offsetX, offsetY);
        return ERROR_FINISHED;
      }
    }
    return ERROR_NONE;
  }
};
#endif