package xpages.gif;

import java.net.*;
import java.io.*;
import java.util.*;
import java.awt.*;
import java.awt.image.*;

/**
 * Class GifDecoder - Decodes a GIF file into one or more frames.
 * <br><pre>
 * Example:
 *    GifDecoder d = new GifDecoder();
 *    d.read("sample.gif");
 *    int n = d.getFrameCount();
 *    for (int i = 0; i < n; i++) {
 *       BufferedImage frame = d.getFrame(i);  // frame i
 *       int t = d.getDelay(i);  // display duration of frame in milliseconds
 *       // do something with frame
 *    }
 * </pre>
 * No copyright asserted on the source code of this class.  May be used for
 * any purpose, however, refer to the Unisys LZW patent for any additional
 * restrictions.  Please forward any corrections to kweiner@fmsware.com.
 *
 * @author Kevin Weiner, FM Software; LZW decoder adapted from John Cristy's ImageMagick.
 * @version 1.01 July 2001
 *
 */

public class GifDecoder {

   /**
    * File read status: No errors.
    */
   public static final int STATUS_OK = 0;

   /**
    * File read status: Error decoding file (may be partially decoded)
    */
   public static final int STATUS_FORMAT_ERROR = 1;

   /**
    * File read status: Unable to open source.
    */
   public static final int STATUS_OPEN_ERROR = 2;

   protected BufferedInputStream in;
   protected int status;

   protected int width;            // full image width
   protected int height;           // full image height
   protected boolean gctFlag;      // global color table used
   protected int gctSize;          // size of global color table
   protected int loopCount = 1;    // iterations; 0 = repeat forever

   protected int[] gct;            // global color table
   protected int[] lct;            // local color table
   protected int[] act;            // active color table

   protected int bgIndex;          // background color index
   protected int bgColor;          // background color
   protected int gbgColor; // global background color (added/Damien)
   protected int lastBgColor;      // previous bg color
   protected int pixelAspect;      // pixel aspect ratio

   protected boolean lctFlag;      // local color table flag
   protected boolean interlace;    // interlace flag
   protected int lctSize;          // local color table size

   protected int ix, iy, iw, ih;   // current image rectangle
   protected Rectangle lastRect;   // last image rect
   protected BufferedImage image;  // current frame
   protected BufferedImage lastImage;  // previous frame

   protected byte[] block = new byte[256];  // current data block
   protected int blockSize = 0;    // block size

   // last graphic control extension info
   protected int dispose = 0;   // 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev
   protected int lastDispose = 0;
   protected boolean transparency = false;   // use transparent color
   protected int delay = 0;        // delay in milliseconds
   protected int transIndex;       // transparent color index

   protected static final int MaxStackSize = 4096;   // max decoder pixel stack size

   // LZW decoder working arrays
   protected short[] prefix;
   protected byte[] suffix;
   protected byte[] pixelStack;
   protected byte[] pixels;

   protected ArrayList<GifFrame> frames;     // frames read from current file
   protected int frameCount;
   
   protected boolean indexation;
   protected boolean palette_globale;

   static class GifFrame {
      public GifFrame(BufferedImage im, int del, boolean trans) {
         image = im;
         delay = del;
         transparency = trans;
      }
      public BufferedImage image;
      public int delay;
      public boolean transparency;
   }


   public void setIndexation(boolean ind) {
      indexation = ind;
   }

   /**
    * Gets display duration for specified frame.
    *
    * @param n int index of frame
    * @return delay in milliseconds
    */
   public int getDelay(int n) {
      //
      delay = -1;
      if ((n >= 0) && (n < frameCount))
         delay = frames.get(n).delay;
      return delay;
   }


   /**
    * Gets transparency duration for specified frame.
    *
    * @param n int index of frame
    * @return boolean transparency
    */
   public boolean getTransparency(int n) {
      //
      boolean transparency = false;
      if ((n >= 0) && (n < frameCount))
         transparency = frames.get(n).transparency;
      return transparency;
   }


   /**
    * Gets dispose.
    * 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev
    *
    * @return int dispose
    */
   public int getDispose() {
      //
      return dispose;
   }


   /**
    * Gets palette_globale (utilisation d'une seule palette dans toute l'animation)
    *
    * @return boolean palette_globale
    */
   public boolean getPaletteGlobale() {
      //
      return palette_globale;
   }

   /**
    * Gets bgIndex (background color index)
    *
    * @return int bgIndex
    */
   public int getBgIndex() {
      //
      return bgIndex;
   }
    
    /**
     * Gets current transIndex (transparent color index)
     */
    public int getTransIndex() {
        return transIndex;
    }
    
    /**
     * Gets the global lookup table, or null if it isn't defined
     *
     * @return global lookup table, or null
     */
    public int[] getGct() {
        return gct;
    }
    
    /**
      * Global background color (0 if there is no global color table)
      */
    public int getBgColor() {
        return(gbgColor);
    }
    
   /**
    * Gets the image contents of frame n.
    *
    * @return BufferedImage representation of frame, or null if n is invalid.
    */
   public BufferedImage getFrame(int n) {
      BufferedImage im = null;
      if ((n >= 0) && (n < frameCount))
         im = frames.get(n).image;
      return im;
   }


   /**
    * Gets the number of frames read from file.
    * @return frame count
    */
   public int getFrameCount() {
      return frameCount;
   }


   /**
    * Gets the first (or only) image read.
    *
    * @return BufferedImage containing first frame, or null if none.
    */
   public BufferedImage getImage() {
      return getFrame(0);
   }


   /**
    * Gets the "Netscape" iteration count, if any.
    * A count of 0 means repeat indefinitiely.
    *
    * @return iteration count if one was specified, else 1.
    */
   public int getLoopCount() {
       return loopCount;
   }


   /**
    * Reads GIF image from stream
    *
    * @param BufferedInputStream containing GIF file.
    * @return read status code (0 = no errors)
    */
   public int read(BufferedInputStream is) {
      init();
      if (is != null) {
         in = is;
         readHeader();
         if (!err()) {
            readContents();
            if (frameCount < 0)
               status = STATUS_FORMAT_ERROR;
         }
      } else {
         status = STATUS_OPEN_ERROR;
      }
      try {
         is.close();
      } catch (IOException e) {}
      return status;
   }


   /**
    * Reads GIF file from specified source (file or URL string)
    *
    * @param name String containing source
    * @return read status code (0 = no errors)
    */
   public int read(String name) {
      status = STATUS_OK;
      try {
         name = name.trim();
         if (name.indexOf("://") > 0) {
            URL url = new URL(name);
            in = new BufferedInputStream(url.openStream());
         } else {
            in = new BufferedInputStream(new FileInputStream(name));
         }
         status = read(in);
      } catch (IOException e) {
         status = STATUS_OPEN_ERROR;
      }

      return status;
   }


   /**
    * Decodes LZW image data into pixel array.
    * Adapted from John Cristy's ImageMagick.
    */
    protected void decodeImageData() {
      int NullCode = -1;
      int npix = iw * ih;
      int available, clear, code_mask, code_size, end_of_information, in_code, old_code,
         bits, code, count, i, datum, data_size, first, top, bi, pi;

      if ((pixels == null) || (pixels.length < npix))
         pixels = new byte[npix];    // allocate new pixel array

      if (prefix == null)
         prefix = new short[MaxStackSize];
      if (suffix == null)
         suffix = new byte[MaxStackSize];
      if (pixelStack == null)
         pixelStack = new byte[MaxStackSize+1];


      //  Initialize GIF data stream decoder.

      data_size = read();
      clear = 1 << data_size;
      end_of_information = clear + 1;
      available = clear + 2;
      old_code = NullCode;
      code_size = data_size + 1;
      code_mask = (1 << code_size) - 1;
      for (code = 0; code < clear; code++) {
         prefix[code] = 0;
         suffix[code] = (byte) code;
      }

      //  Decode GIF pixel stream.

      datum = bits = count = first = top = pi = bi = 0;

      for (i = 0; i < npix; ) {
         if (top == 0) {
            if (bits < code_size) {
               //  Load bytes until there are enough bits for a code.
               if (count == 0) {
                  // Read a new data block.
                  count = readBlock();
                  if (count <= 0)
                     break;
                  bi = 0;
               }
               datum += (((int) block[bi]) & 0xff) << bits;
               bits += 8;
               bi++;
               count--;
               continue;
            }

            //  Get the next code.

            code = datum & code_mask;
            datum >>= code_size;
            bits -= code_size;

            //  Interpret the code

            if ((code > available) || (code == end_of_information))
               break;
            if (code == clear) {
               //  Reset decoder.
               code_size = data_size + 1;
               code_mask = (1 << code_size) - 1;
               available = clear + 2;
               old_code = NullCode;
               continue;
            }
            if (old_code == NullCode) {
               pixelStack[top++] = suffix[code];
               old_code = code;
               first = code;
               continue;
            }
            in_code = code;
            if (code == available) {
               pixelStack[top++] = (byte) first;
               code = old_code;
            }
            while (code > clear) {
               pixelStack[top++] = suffix[code];
               code = prefix[code];
            }
            first = ((int) suffix[code]) & 0xff;

            //  Add a new string to the string table,

            if (available >= MaxStackSize)
               break;
            pixelStack[top++] = (byte) first;
            prefix[available] = (short) old_code;
            suffix[available] = (byte) first;
            available++;
            if (((available & code_mask) == 0) && (available < MaxStackSize)) {
               code_size++;
               code_mask += available;
            }
            old_code = in_code;
         }

         //  Pop a pixel off the pixel stack.

         top--;
         pixels[pi++] = pixelStack[top];
         i++;
      }

      for (i = pi; i < npix; i++)
         pixels[i] = 0;  // clear missing pixels

   }


   /**
    * Returns true if an error was encountered during reading/decoding
    */
   protected boolean err() {
      return status != STATUS_OK;
   }


   /**
    * Initializes or re-initializes reader
    */
   protected void init() {
       status = STATUS_OK;
      frameCount = 0;
      frames = new ArrayList<GifFrame>();
      gct = null;
      lct = null;
   }


   /**
    * Reads a single byte from the input stream.
    */
   protected int read() {
      int curByte = 0;
      try {
         curByte = in.read();
      } catch (IOException e) {
         status = STATUS_FORMAT_ERROR;
      }
      return curByte;
   }


   /**
    * Reads next variable length block from input.
    *
    * @return number of bytes stored in "buffer"
    */
   protected int readBlock() {
      blockSize = read();
      int n = 0;
      if (blockSize > 0) {
         try {
            int count = 0;
            while (n<blockSize) {
               count = in.read(block, n, blockSize-n);
               if (count==-1)
                  break;
               n += count;
            }
         } catch (IOException e) {}

         if (n < blockSize)
            status = STATUS_FORMAT_ERROR;
      }
      return n;
   }


   /**
    * Reads color table as 256 RGB integer values
    *
    * @param ncolors int number of colors to read
    * @return int array containing 256 colors (packed ARGB with full alpha)
    */
   protected int[] readColorTable(int ncolors) {
      int nbytes = 3*ncolors;
      int[] tab = null;
      byte[] c = new byte[nbytes];
      int n = 0;
      try {
         n = in.read(c);
      } catch (IOException e) {}
      if (n < nbytes)
         status = STATUS_FORMAT_ERROR;
      else {
         tab = new int[256];  // max size to avoid bounds checks
         int i = 0;
         int j = 0;
         while (i < ncolors) {
            int r = ((int) c[j++]) & 0xff;
            int g = ((int) c[j++]) & 0xff;
            int b = ((int) c[j++]) & 0xff;
            tab[i++] = 0xff000000 | (r << 16) | (g << 8) | b;
         }
      }
      return tab;
   }


   /**
    * Main file parser.  Reads GIF content blocks.
    */
   protected void readContents() {
      // read GIF file content blocks
      boolean done = false;
      while (!(done || err())) {
         int code = read();
         switch (code) {

            case 0x2C:    // image separator
               readImage();
               break;

            case 0x21:    // extension
               code = read();
               switch (code) {

                  case 0xf9:    // graphics control extension
                     readGraphicControlExt();
                     break;

                  case 0xff:    // application extension
                     readBlock();
                     String app = "";
                     for (int i = 0; i < 11; i++)
                        app += (char) block[i];
                     if (app.equals("NETSCAPE2.0"))
                        readNetscapeExt();
                     else
                        skip();        // don't care
                     break;

                  default:    // uninteresting extension
                     skip();
               }
               break;

            case 0x3b:        // terminator
               done = true;
               break;

            default:
               status = STATUS_FORMAT_ERROR;
         }
      }
   }


   /**
    * Reads Graphics Control Extension values
    */
   protected void readGraphicControlExt() {
      read();    // block size
      int packed = read();   // packed fields
      dispose = (packed & 0x1c) >> 2;   // disposal method
      if (dispose == 0)
         dispose = 1;   // elect to keep old image if discretionary
      transparency = (packed & 1) != 0;
      delay = readShort() * 10;   // delay in milliseconds
      transIndex = read();        // transparent color index
      read();                     // block terminator
   }


   /**
    * Reads GIF file header information.
    */
   protected void readHeader() {
      String id = "";
      for (int i = 0; i < 6; i++)
         id += (char) read();
      if (!id.startsWith("GIF")) {
         status = STATUS_FORMAT_ERROR;
         return;
      }

      readLSD();
      if (gctFlag && !err()) {
         gct = readColorTable(gctSize);
         bgColor = gct[bgIndex];
         gbgColor = bgColor;
      }
   }


   /**
    * Reads next frame image
    */
   protected void readImage() {
      ix = readShort();    // (sub)image position & size
      iy = readShort();
      iw = readShort();
      ih = readShort();

      int packed = read();
      lctFlag = (packed & 0x80) != 0;     // 1 - local color table flag
      if (lctFlag)
          palette_globale = false;
      interlace = (packed & 0x40) != 0;   // 2 - interlace flag
      // 3 - sort flag
      // 4-5 - reserved
      lctSize = 2 << (packed & 7);        // 6-8 - local color table size

      if (lctFlag) {
         lct = readColorTable(lctSize);   // read table
         act = lct;    // make local table active
      } else {
         act = gct;    // make global table active
         if (bgIndex == transIndex)
            bgColor = 0;
      }
      int save = 0;
      if (transparency) {
         save = act[transIndex];
         act[transIndex] = 0;    // set transparent color if specified
      }

      if (act == null)
         status = STATUS_FORMAT_ERROR;   // no color table defined

      if (err()) return;

      decodeImageData();   // decode pixel data
      skip();

      if (err()) return;

      frameCount++;
    
      if (indexation) {
        // creer la palette de couleurs
        if (transparency)
           act[transIndex] = save; 
        IndexColorModel cm = new IndexColorModel(8, act.length, act, 0, true, transparency?transIndex:-1, DataBuffer.TYPE_BYTE);
        if (transparency)
           act[transIndex] = 0; 
        
        // create new image to receive frame data
        image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, cm);
        
      } else {
        // create new image to receive frame data
        image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
      }

      setPixels();   // transfer pixel data to image

      frames.add(new GifFrame(image, delay, transparency));   // add image to frame list

      if (transparency)
         act[transIndex] = save;
      resetFrame();

   }


   /**
    * Reads Logical Screen Descriptor
    */
   protected void readLSD() {

      // logical screen size
      width = readShort();
      height = readShort();

      // packed fields
      int packed = read();
      gctFlag = (packed & 0x80) != 0;      // 1   : global color table flag
      palette_globale = gctFlag;
      // 2-4 : color resolution
      // 5   : gct sort flag
      gctSize    = 2 << (packed & 7);      // 6-8 : gct size

      bgIndex = read();        // background color index
      pixelAspect = read();    // pixel aspect ratio
   }


   /**
    * Reads Netscape extenstion to obtain iteration count
    */
   protected void readNetscapeExt() {
      do {
         readBlock();
         if (block[0] == 1) {
            // loop count sub-block
            int b1 = ((int) block[1]) & 0xff;
            int b2 = ((int) block[2]) & 0xff;
            loopCount = (b2 << 8) | b1;
         }
      } while ((blockSize > 0) && !err());
   }


   /**
    * Reads next 16-bit value, LSB first
    */
   protected int readShort() {
      // read 16-bit value, LSB first
      return read() | (read() << 8);
   }


   /**
    * Resets frame state for reading next image.
    */
   protected void resetFrame() {
      lastDispose = dispose;
      lastRect = new Rectangle(ix, iy, iw, ih);
      lastImage = image;
      lastBgColor = bgColor;
      int dispose = 0;
      boolean transparency = false;
      int delay = 0;
      lct = null;
   }


   /**
    * Creates new frame image from current data (and previous
    * frames as specified by their disposition codes).
    */
   protected void setPixels() {
      int[] dest = null;
      byte[] destind = null;
      
      // expose destination image's pixels as int array
      if (!indexation)
         dest = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
      else
         destind = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();

      // fill in starting image contents based on last image's dispose code
      if (lastDispose > 0) {
         if (lastDispose == 3) {
            // use image before last
            int n = frameCount - 2;
            if (n > 0)
               lastImage = getFrame(n-1);
            else
               lastImage = null;
         }

         if (lastImage != null) {
            int[] prev;
            byte[] prevind;
            if (!indexation) {
                prev = ((DataBufferInt) lastImage.getRaster().getDataBuffer()).getData();
                System.arraycopy(prev, 0, dest, 0, width*height);  // copy pixels
            } else {
                prevind = ((DataBufferByte) lastImage.getRaster().getDataBuffer()).getData();
                System.arraycopy(prevind, 0, destind, 0, width*height);  // copy pixels
            }

            if (lastDispose == 2) {
               // fill last image rect area with background color
               if (indexation)
                   for (int i=0; i<width*height; i++)
                       destind[i] = (byte)bgIndex;
               else
                   for (int i=0; i<width*height; i++)
                       dest[i] = lastBgColor;
            }
         }
      }

      // copy each source line to the appropriate place in the destination
      int pass = 1;
      int inc = 8;
      int iline = 0;
      for (int i = 0; i < ih; i++) {
         int line = i;
         if (interlace) {
            if (iline >= ih) {
               pass++;
               switch (pass) {
                  case 2:
                     iline = 4;
                     break;
                  case 3:
                     iline = 2;
                     inc = 4;
                     break;
                  case 4:
                     iline = 1;
                     inc = 2;
               }
            }
            line = iline;
            iline += inc;
         }
         line += iy;
         if (line < height) {
            int k = line * width;
            int dx = k + ix;          // start of line in dest
            int dlim = dx + iw;       // end of dest line
            if ((k + width) < dlim)
               dlim = k + width;      // past dest edge
            int sx = i * iw;          // start of line in source
            while (dx < dlim) {
               // map color and insert in destination
               if (indexation) {
                   byte index = (byte)( ((int) pixels[sx++]) & 0xff );
                   // ajout de ce test le 21/8/2009 pour les anims optimisees avec gifsicle (changement de l'index transparent) :
                   if (!transparency || index != transIndex)
                       destind[dx] = index;
               } else {
                   int index = ((int) pixels[sx++]) & 0xff;
                   int c = act[index];
                   if (c != 0)
                       dest[dx] = c;
               }
               dx++;
            }
         }
      }
   }


   /**
    * Skips variable length blocks up to and including
    * next zero length block.
    */
   protected void skip() {
      do {
         readBlock();
      } while ((blockSize > 0) && !err());
   }
}
