/*
  Cockos WDL - LICE - Lightweight Image Compositing Engine
  Copyright (C) 2007 and later, Cockos Incorporated
  File: lice_gif.cpp (GIF loading for LICE)
  See lice.h for license and other information
*/

#include "lice.h"

#include <stdio.h>

#include "../wdltypes.h"
#include "../filewrite.h"

extern "C" {

#include "../giflib/gif_lib.h"
//int _GifError;
};


struct liceGifWriteRec
{
  GifFileType *f;
  WDL_FileWrite *fh;
  ColorMapObject *cmap;
  GifPixelType *linebuf;
  LICE_IBitmap *prevframe; // used when multiframe, transalpha<0
  void *last_octree;
  LICE_pixel last_palette[256];
  unsigned char from15to8bit[32][32][32];//r,g,b

  int transalpha;
  int w,h;
  bool append;
  bool dither;
  bool has_had_frame;
  bool has_global_cmap; 

  bool has_from15to8bit; // set when last_octree has been generated into from15to8bit
};

static inline GifPixelType QuantPixel(LICE_pixel p, liceGifWriteRec *wr)
{
  return wr->from15to8bit[LICE_GETR(p)>>3][LICE_GETG(p)>>3][LICE_GETB(p)>>3];
}

static int generate_palette_from_octree(void *ww, void *octree, int numcolors)
{
  liceGifWriteRec  *wr = (liceGifWriteRec *)ww;
  if (!octree||!ww||numcolors>256) return 0;

  ColorMapObject *cmap = wr->cmap;
  
  int palette_sz=0;

  // store palette
  {
    LICE_pixel* palette=wr->last_palette;

    palette_sz = LICE_ExtractOctreePalette(octree, palette);

    int i;
    for (i = 0; i < palette_sz; ++i)
    {
      cmap->Colors[i].Red = LICE_GETR(palette[i]);
      cmap->Colors[i].Green = LICE_GETG(palette[i]);
      cmap->Colors[i].Blue = LICE_GETB(palette[i]);
    }
    for (i = palette_sz; i < numcolors; ++i)
    {
      cmap->Colors[i].Red = cmap->Colors[i].Green = cmap->Colors[i].Blue = 0;   
    }
  }

  wr->has_from15to8bit = false;
  wr->has_global_cmap=true;

  return palette_sz;
}

static void generate15to8(void *ww, void *octree)
{
  liceGifWriteRec  *wr = (liceGifWriteRec *)ww;
  if (!octree||!ww) return;

  // map palette to 16 bit
  unsigned char r,g,b;
  for(r=0;r<32;r++)  
  {
    unsigned char cr = r<<3;
    for (g=0;g<32;g++)
    {
      unsigned char cg = g<<3;
      for (b=0;b<32;b++)
      {
        unsigned char cb = b<<3;
        LICE_pixel col = LICE_RGBA(cr,cg,cb,0);
        wr->from15to8bit[r][g][b] = LICE_FindInOctree(octree, col);
      }
    }
  }
  wr->has_from15to8bit=true;
}

int LICE_SetGIFColorMapFromOctree(void *ww, void *octree, int numcolors)
{
  const int rv = generate_palette_from_octree(ww,octree,numcolors);
  generate15to8(ww,octree);
  return rv;
}

unsigned int LICE_WriteGIFGetSize(void *handle)
{
  if (handle)
  {
    liceGifWriteRec *wr = (liceGifWriteRec*)handle;
    if (wr->fh) return (unsigned int) ((WDL_FileWrite *)wr->fh)->GetPosition();
  }
  return 0;
}

bool LICE_WriteGIFFrame(void *handle, LICE_IBitmap *frame, int xpos, int ypos, bool perImageColorMap, int frame_delay, int nreps)
{
  liceGifWriteRec *wr = (liceGifWriteRec*)handle;
  if (!wr) return false;

  bool isFirst=false;
  if (!wr->has_had_frame)
  {
    wr->has_had_frame=true;
    isFirst=true;

    if (!perImageColorMap && !wr->has_global_cmap)
    {
      const int ccnt = 256 - (wr->transalpha?1:0);
      void* octree = wr->last_octree;
      if (!octree) wr->last_octree = octree = LICE_CreateOctree(ccnt);
      else LICE_ResetOctree(octree,ccnt);

      if (octree) 
      {
        LICE_BuildOctree(octree, frame);
        // sets has_global_cmap
        int pcnt = generate_palette_from_octree(wr, octree, ccnt);

        if (pcnt < 256 && wr->transalpha) pcnt++;
        int nb = 1;
        while (nb < 8 && (1<<nb) < pcnt) nb++;
        wr->cmap->ColorCount = 1<<nb;
        wr->cmap->BitsPerPixel=nb;
      }
    }

    if (!wr->append) EGifPutScreenDesc(wr->f,wr->w,wr->h,8,0,wr->has_global_cmap ? wr->cmap : 0);

  }

  int usew=frame->getWidth(), useh=frame->getHeight();
  if (xpos+usew > wr->w) usew = wr->w-xpos;
  if (ypos+useh > wr->h) useh = wr->h-ypos;
  if (usew<1||useh<1) return false;

  int pixcnt=usew*useh;

  const int trans_chan_mask = wr->transalpha&0xff; // -1 means 0xff by default, user can change this accordingly
  const LICE_pixel trans_mask = LICE_RGBA(trans_chan_mask,trans_chan_mask,trans_chan_mask,0);
  const bool advanced_trans_stats = !!(wr->transalpha&0x100);

  if (perImageColorMap && !wr->has_global_cmap)
  {
    const int ccnt = 256 - (wr->transalpha?1:0);
    void* octree = wr->last_octree;
    if (!octree) wr->last_octree = octree = LICE_CreateOctree(ccnt);
    else LICE_ResetOctree(octree,ccnt);
    if (octree) 
    {
      if ((!isFirst || frame_delay) && wr->transalpha<0 && wr->prevframe)
      {
        LICE_SubBitmap tmpprev(wr->prevframe, xpos, ypos, usew, useh);
        int pc=LICE_BuildOctreeForDiff(octree,frame,&tmpprev,trans_mask);
        if (!advanced_trans_stats) pixcnt = pc;
      }
      else if (wr->transalpha>0)
        pixcnt=LICE_BuildOctreeForAlpha(octree, frame,wr->transalpha&0xff);
      else
        LICE_BuildOctree(octree, frame);

        // sets has_global_cmap (clear below)
      int pcnt = generate_palette_from_octree(wr, octree, ccnt);

      wr->has_global_cmap=false;
      if (pcnt < 256 && wr->transalpha) pcnt++;
      int nb = 1;
      while (nb < 8 && (1<<nb) < pcnt) nb++;
      wr->cmap->ColorCount = 1<<nb;
      wr->cmap->BitsPerPixel=nb;
    }
  }

  if (!wr->has_from15to8bit && pixcnt > 40000 && wr->last_octree)
  {
    generate15to8(wr,wr->last_octree);
  }

  const unsigned char transparent_pix = wr->cmap->ColorCount-1;
  unsigned char gce[4] = { 0, };
  if (wr->transalpha)
  {
    gce[0] |= 1;
    gce[3] = transparent_pix;
  }

  int a = frame_delay/10;
  if(a<2&&frame_delay)a=2; // https://www.biphelps.com/blog/The-Fastest-GIF-Does-Not-Exist
  else if (a>60000) a=60000;
  gce[1]=(a)&255;
  gce[2]=(a)>>8;

  if (isFirst && frame_delay && nreps!=1 && !wr->append)
  {
    int nr = nreps > 1 && nreps <= 65536 ? nreps-1 : 0;
    unsigned char ext[]={0xB, 'N','E','T','S','C','A','P','E','2','.','0',3,1,(unsigned char) (nr&0xff), (unsigned char) ((nr>>8)&0xff)};
    EGifPutExtension(wr->f,0xFF, sizeof(ext),ext);
  }

  if (gce[0]||gce[1]||gce[2])
    EGifPutExtension(wr->f, 0xF9, sizeof(gce), gce);


  EGifPutImageDesc(wr->f, xpos, ypos, usew,useh, 0, wr->has_global_cmap ? NULL : wr->cmap); 

  GifPixelType *linebuf = wr->linebuf;
  int y;

  void *use_octree = wr->has_from15to8bit ? NULL : wr->last_octree;

  if ((!isFirst || frame_delay) && wr->transalpha<0)
  {
    bool ignFr=false;
    if (!wr->prevframe)
    {
      ignFr=true;
      wr->prevframe = new WDL_NEW LICE_MemBitmap(wr->w,wr->h);
      LICE_Clear(wr->prevframe,0);
    }

    LICE_SubBitmap tmp(wr->prevframe,xpos,ypos,usew,useh);

    LICE_pixel last_pixel_rgb=0;
    GifPixelType last_pixel_idx=transparent_pix;

    int pix_stats[256];
    if (advanced_trans_stats) memset(pix_stats,0,sizeof(pix_stats));
    pix_stats[transparent_pix] = -8;

    for(y=0;y<useh;y++)
    {
      int rdy=y,rdy2=y;
      if (frame->isFlipped()) rdy = frame->getHeight()-1-y;
      if (tmp.isFlipped()) rdy2 = tmp.getHeight()-1-y;
      const LICE_pixel *in = frame->getBits() + rdy*frame->getRowSpan();
      const LICE_pixel *in2 = tmp.getBits() + rdy2*tmp.getRowSpan();
      int x;

      if (advanced_trans_stats)
      {
        if (use_octree) for(x=0;x<usew;x++)
        {
          const LICE_pixel p = in[x]&trans_mask;
          if (last_pixel_idx == transparent_pix || last_pixel_rgb!=p)
          {
            if (ignFr || p != (in2[x]&trans_mask)) last_pixel_idx = LICE_FindInOctree(use_octree,p);
            else 
            {
              const GifPixelType np = LICE_FindInOctree(use_octree,p);
              if (p != (wr->last_palette[np]&trans_mask) || pix_stats[transparent_pix] > pix_stats[np])
                last_pixel_idx = transparent_pix;
              else 
                last_pixel_idx = np;
            }
          }
          linebuf[x] = last_pixel_idx;
          pix_stats[last_pixel_idx]++;
          last_pixel_rgb = p;
        }
        else for(x=0;x<usew;x++)
        {
          const LICE_pixel p = in[x]&trans_mask;
          if (last_pixel_idx == transparent_pix || last_pixel_rgb!=p)
          {
            if (ignFr || p != (in2[x]&trans_mask)) last_pixel_idx = QuantPixel(p,wr);
            else 
            {
              const GifPixelType np = QuantPixel(p,wr);

              if (p != (wr->last_palette[np]&trans_mask) || pix_stats[transparent_pix] > pix_stats[np])
                last_pixel_idx = transparent_pix;
              else 
                last_pixel_idx = np;
            }
          }
          linebuf[x] = last_pixel_idx;
          pix_stats[last_pixel_idx]++;
          last_pixel_rgb = p;
        }
      }
      else
      {
        // optimize solids by reusing the same value if previous rgb was the same, also avoid switching between
        // from color to transparent if the color hasn't changed
        if (use_octree) for(x=0;x<usew;x++)
        {
          const LICE_pixel p = in[x]&trans_mask;
          if (last_pixel_idx == transparent_pix || last_pixel_rgb!=p)
          {
            if (ignFr || p != (in2[x]&trans_mask)) last_pixel_idx = LICE_FindInOctree(use_octree,last_pixel_rgb = p);
            else last_pixel_idx = transparent_pix;
          }
          linebuf[x] = last_pixel_idx;
        }
        else for(x=0;x<usew;x++)
        {
          const LICE_pixel p = in[x]&trans_mask;
          if (last_pixel_idx == transparent_pix || last_pixel_rgb!=p)
          {
            if (ignFr || p != (in2[x]&trans_mask)) last_pixel_idx = QuantPixel(last_pixel_rgb = p,wr);
            else last_pixel_idx = transparent_pix;
          }
          linebuf[x] = last_pixel_idx;
        }
      }


      EGifPutLine(wr->f, linebuf, usew);
    }

    LICE_Blit(&tmp,frame,0,0,0,0,usew,useh,1.0f,LICE_BLIT_MODE_COPY);
    
  }
  else if (wr->transalpha>0)
  {
    const unsigned int al = wr->transalpha&0xff;
    for(y=0;y<useh;y++)
    {
      int rdy=y;
      if (frame->isFlipped()) rdy = frame->getHeight()-1-y;
      const LICE_pixel *in = frame->getBits() + rdy*frame->getRowSpan();
      int x;
      if (use_octree) for(x=0;x<usew;x++)
      {
        const LICE_pixel p = in[x];
        if (LICE_GETA(p)<al) linebuf[x]=transparent_pix;
        else linebuf[x] = LICE_FindInOctree(use_octree,p);
      }
      else for(x=0;x<usew;x++)
      {
        const LICE_pixel p = in[x];
        if (LICE_GETA(p)<al) linebuf[x]=transparent_pix;
        else linebuf[x] = QuantPixel(p,wr);
      }
      EGifPutLine(wr->f, linebuf, usew);
    }
  }
  else for(y=0;y<useh;y++)
  {
    int rdy=y;
    if (frame->isFlipped()) rdy = frame->getHeight()-1-y;
    const LICE_pixel *in = frame->getBits() + rdy*frame->getRowSpan();
    int x;
    if (use_octree) for(x=0;x<usew;x++) linebuf[x] = LICE_FindInOctree(use_octree,in[x]);
    else for(x=0;x<usew;x++) linebuf[x] = QuantPixel(in[x],wr);
    EGifPutLine(wr->f, linebuf, usew);
  }

  return true;
}

static int writefunc_fh(GifFileType *fh, const GifByteType *buf, int sz) 
{  
  return ((WDL_FileWrite *)fh->UserData)->Write(buf,sz);
}

void *LICE_WriteGIFBeginNoFrame(const char *filename, int w, int h, int transparent_alpha, bool dither, bool is_append)
{
  WDL_FileWrite *fp = new WDL_FileWrite(filename,1,65536,16,16,is_append);
  if (!fp->IsOpen()) 
  {
    delete fp;
    return NULL;
  }


  EGifSetGifVersion("89a");

  
  GifFileType *f = EGifOpen(fp,writefunc_fh);
  if (!f) 
  {
    delete fp;
    return NULL;
  }

  liceGifWriteRec *wr = (liceGifWriteRec*)calloc(sizeof(liceGifWriteRec),1);
  wr->f = f;
  wr->fh = fp;
  wr->append = is_append;
  wr->dither = dither;
  wr->w=w;
  wr->h=h;
  wr->cmap = (ColorMapObject*)calloc(sizeof(ColorMapObject)+256*sizeof(GifColorType),1);
  wr->cmap->Colors = (GifColorType*)(wr->cmap+1);
  wr->cmap->ColorCount=256;
  wr->cmap->BitsPerPixel=8;
  wr->has_had_frame=false;
  wr->has_global_cmap=false;
  wr->has_from15to8bit=false;
  wr->last_octree=NULL;

  wr->linebuf = (GifPixelType*)malloc(wr->w*sizeof(GifPixelType));
  wr->transalpha = transparent_alpha;

  return wr;
}
void *LICE_WriteGIFBegin(const char *filename, LICE_IBitmap *firstframe, int transparent_alpha, int frame_delay, bool dither, int nreps)
{
  if (!firstframe) return NULL;

  void *wr=LICE_WriteGIFBeginNoFrame(filename,firstframe->getWidth(),firstframe->getHeight(),transparent_alpha,dither);
  if (wr) LICE_WriteGIFFrame(wr,firstframe,0,0,true,frame_delay,nreps);

  return wr;
}



bool LICE_WriteGIFEnd(void *handle)
{
  liceGifWriteRec *wr = (liceGifWriteRec*)handle;
  if (!wr) return false;

  int ret = EGifCloseFile(wr->f);

  free(wr->linebuf);
  free(wr->cmap);
  if (wr->last_octree) LICE_DestroyOctree(wr->last_octree);

  delete wr->prevframe;
  delete wr->fh;

  free(wr);

  return ret!=GIF_ERROR;
}


bool LICE_WriteGIF(const char *filename, LICE_IBitmap *bmp, int transparent_alpha, bool dither)
{
  // todo: alpha?
  if (!bmp) return false;

  int has_transparent = 0;
  if (transparent_alpha>0)
  {
    int y=bmp->getHeight();
    LICE_pixel *p = bmp->getBits();
    int w = bmp->getWidth();
    while (y--&&!has_transparent)
    {
      int x=w;
      while(x--)
      {
        if (LICE_GETA(*p) < (unsigned int)transparent_alpha)
        {
          has_transparent=1;
          break;
        }
        p++;
      }
      p+=bmp->getRowSpan()-w;
    }
  }


  void *wr=LICE_WriteGIFBeginNoFrame(filename,bmp->getWidth(),bmp->getHeight(),has_transparent?transparent_alpha:0,dither);
  if (!wr)  return false;
  
  LICE_WriteGIFFrame(wr,bmp,0,0,false,0);

  return LICE_WriteGIFEnd(wr);
}
