/*
 * (C) 2004 - Geotechnical Software Services
 * 
 * This code is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public 
 * License as published by the Free Software Foundation; either 
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This code is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public 
 * License along with this program; if not, write to the Free 
 * Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, 
 * MA  02111-1307, USA.
 */
package no.geosoft.cc.io;



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



/**
 * Class for converting images to GIF files.
 *
 * <p>
 * Contribution:
 * <ul>
 *   <li>Sverre H. Huseby (gifsave.c on which this is based)</li>
 *   <li>Adam Doppelt (Initial Java port)</li>
 *   <li>Greg Faron (Initial java port)</li>
 * </ul>
 * 
 * @author <a href="mailto:info@geosoft.no">GeoSoft</a>
 */   
public class GifEncoder
{
  private short   imageWidth_, imageHeight_;
  private int     nColors_;
  private byte[]  pixels_ = null;
  private byte[]  colors_ = null;



  /**
   * Constructing a GIF encoder.
   *
   * @param image  The image to encode. The image must be
   *               completely loaded.
   * @throws AWTException  If memory is exhausted or image contains
   *                       more than 256 colors.
   */
  public GifEncoder (Image image)
    throws AWTException
  {
    imageWidth_  = (short) image.getWidth (null);
    imageHeight_ = (short) image.getHeight (null);

    int values[] = new int[imageWidth_ * imageHeight_];
    PixelGrabber grabber = new PixelGrabber (image, 0, 0,
                                             imageWidth_, imageHeight_,
                                             values, 0, imageWidth_);

    try {
      if (grabber.grabPixels() != true)
        throw new AWTException("Grabber returned false: " + grabber.status());
    }
    catch (InterruptedException exception) {
    }

    byte[][] r = new byte[imageWidth_][imageHeight_];
    byte[][] g = new byte[imageWidth_][imageHeight_];
    byte[][] b = new byte[imageWidth_][imageHeight_];

    int index = 0;

    for (int y = 0; y < imageHeight_; y ++) {
      for (int x = 0; x < imageWidth_; x ++, index ++) {
        r[x][y] = (byte) ((values[index] >> 16) & 0xFF);
        g[x][y] = (byte) ((values[index] >> 8)  & 0xFF);
        b[x][y] = (byte) ((values[index] >> 0)  & 0xFF);
      }
    }

    toIndexColor (r, g, b);
  }


  
  /**
   * Create a GIF encoder. r[i][j] refers to the pixel at
   * column i, row j.
   *
   * @param r  Red intensity values.
   * @param g  Green intensity values.
   * @param b  Blue intensity values.
   * @throws AWTException  If memory is exhausted or image contains
   *                       more than 256 colors.
   */
  public GifEncoder (byte[][] r, byte[][] g, byte[][] b)
    throws AWTException
  {
    imageWidth_  = (short) (r.length);
    imageHeight_ = (short) (r[0].length);

    toIndexColor(r, g, b);
  }


  
  /**
   * Write image to GIF file.
   * 
   * @param image  Image to write.
   * @param file   File to erite to.
   */
  public static void writeFile (Image image, File file)
    throws AWTException, IOException
  {
    GifEncoder gifEncoder = new GifEncoder (image);
    gifEncoder.write (new FileOutputStream (file));
  }
  

  
  /**
   * Write AWT/Swing component to GIF file.
   * 
   * @param image  Image to write.
   * @param file   File to erite to.
   */
  public static void writeFile (Component component, File file)
    throws AWTException, IOException
  {
    Image image  = component.createImage (component.getWidth(),
                                          component.getHeight());
    Graphics graphics = image.getGraphics();
    component.printAll (graphics);

    GifEncoder.writeFile (image, file);
  }
  
  
  
  /**
   * Writes the image out to a stream in GIF format.
   * This will be a single GIF87a image, non-interlaced, with no
   * background color.
   *
   * @param  stream       The stream to which to output.
   * @throws IOException  Thrown if a write operation fails.
   */
  public void write (OutputStream stream)
    throws IOException
  {
    writeString (stream, "GIF87a");
    writeScreenDescriptor (stream);

    stream.write (colors_, 0, colors_.length);

    writeImageDescriptor (stream, imageWidth_, imageHeight_, ',');

    byte codeSize = bitsNeeded (nColors_);
    if (codeSize == 1) codeSize++;

    stream.write (codeSize);

    writeLzwCompressed (stream, codeSize, pixels_);
    stream.write(0);

    writeImageDescriptor (stream, (short) 0, (short) 0, ';');
    stream.flush();
    stream.close();
  }


  
  /**
   * Converts rgb desrcription of image to colour
   * number description used by GIF.
   *
   * @param r  Red array of pixels.
   * @param g  Green array of pixels.
   * @param b  Blue array of pixels.
   * @throws   AWTException
   *           Thrown if too many different colours in image.
   */
  private void toIndexColor (byte[][] r, byte[][] g, byte[][] b)
    throws AWTException
  {
    pixels_ = new byte[imageWidth_ * imageHeight_];
    colors_ = new byte[256 * 3];
    int colornum = 0;

    for (int x = 0; x < imageWidth_; x++) {
      for (int y = 0; y < imageHeight_; y++ ) {
        int search;
        for (search = 0; search < colornum; search ++ ) {
          if (colors_[search * 3 + 0] == r[x][y] &&
              colors_[search * 3 + 1] == g[x][y] &&
              colors_[search * 3 + 2] == b[x][y]) {
            break;
          }
        }
        
        if (search > 255)
          throw new AWTException("Too many colors.");

        // Row major order y=row x=col
        pixels_[y * imageWidth_ + x] = (byte) search;
        
        if (search == colornum) {
          colors_[search * 3 + 0] = r[x][y]; // [col][row]
          colors_[search * 3 + 1] = g[x][y];
          colors_[search * 3 + 2] = b[x][y];
          colornum++;
        }
      }
    }
    
    nColors_ = 1 << bitsNeeded (colornum);
    byte copy[] = new byte[nColors_ * 3];
    System.arraycopy (colors_, 0, copy, 0, nColors_ * 3);

    colors_ = copy;
  }



  private byte bitsNeeded (int n)
  {
    if (n-- == 0) return 0;

    byte nBitsNeeded = 1;
    while ((n >>= 1) != 0)
      nBitsNeeded++;

    return nBitsNeeded;
  }

  

  private void writeWord (OutputStream stream, short w)
    throws IOException
  {
    stream.write (w & 0xFF);
    stream.write ((w >> 8) & 0xFF);
  }


  
  private void writeString (OutputStream stream, String string)
    throws IOException
  {
    for (int i = 0; i < string.length(); i ++ )
      stream.write ((byte) (string.charAt (i)));
  }



  private void writeScreenDescriptor (OutputStream stream)
    throws IOException
  {
    writeWord (stream, imageWidth_);
    writeWord (stream, imageHeight_);

    byte flag = 0;

    // Global color table size
    byte globalColorTableSize = (byte) (bitsNeeded (nColors_) - 1);
    flag |= globalColorTableSize & 7;

    // Global color table flag
    byte globalColorTableFlag = 1;
    flag |= (globalColorTableFlag & 1) << 7;

    // Sort flag
    byte sortFlag = 0;
    flag |= (sortFlag & 1) << 3;

    // Color resolution
    byte colorResolution = 7;
    flag |= (colorResolution & 7) << 4;    
    
    byte backgroundColorIndex = 0;
    byte pixelAspectRatio     = 0;

    stream.write (flag);
    stream.write (backgroundColorIndex);
    stream.write (pixelAspectRatio);
  }
  


  private void writeImageDescriptor (OutputStream stream,
                                     short width, short height, char separator)
    throws IOException
  {
    stream.write (separator);

    short leftPosition = 0;
    short topPosition  = 0;
    
    writeWord (stream, leftPosition);
    writeWord (stream, topPosition);
    writeWord (stream, width);
    writeWord (stream, height);

    byte flag = 0;
    
    // Local color table size
    byte localColorTableSize = 0;
    flag |= (localColorTableSize & 7);    

    // Reserved
    byte reserved = 0;
    flag |= (reserved & 3) << 3;    

    // Sort flag
    byte sortFlag = 0;
    flag |= (sortFlag & 1) << 5;    

    // Interlace flag
    byte interlaceFlag = 0;
    flag |= (interlaceFlag & 1) << 6;    

    // Local color table flag
    byte localColorTableFlag = 0;
    flag |= (localColorTableFlag & 1) << 7;    

    stream.write (flag);
  }


  
  private void writeLzwCompressed (OutputStream stream, int codeSize,
                                   byte toCompress[])
    throws IOException
  {
    byte c;
    short index;
    int clearcode, endofinfo, numbits, limit, errcode;
    short prefix = (short) 0xFFFF;

    BitFile bitFile = new BitFile (stream);
    LzwStringTable strings = new LzwStringTable();

    clearcode = 1 << codeSize;
    endofinfo = clearcode + 1;

    numbits = codeSize + 1;
    limit = (1 << numbits) - 1;

    strings.clearTable (codeSize);
    bitFile.writeBits(clearcode, numbits);

    for ( int loop = 0; loop < toCompress.length; loop ++ ) {
      c = toCompress[loop];
      if ( (index = strings.findCharString(prefix, c)) != -1 )
        prefix = index;
      else {
        bitFile.writeBits(prefix, numbits);
        if ( strings.addCharString(prefix, c) > limit ) {
          if ( ++ numbits > 12 ) {
            bitFile.writeBits(clearcode, numbits - 1);
            strings.clearTable (codeSize);
            numbits = codeSize + 1;
          }
          
          limit = (1 << numbits) - 1;
        }
        
        prefix = (short) ((short) c & 0xFF);
      }
    }
    
    if ( prefix != -1 )
      bitFile.writeBits(prefix, numbits);
    
    bitFile.writeBits(endofinfo, numbits);
    bitFile.flush();
  }
  


  /**
   * Used to compress the image by looking for repeating
   * elements.
   */
  private class LzwStringTable
  {
    private final static int    RES_CODES  = 2;
    private final static short  HASH_FREE  = (short) 0xFFFF;
    private final static short  NEXT_FIRST = (short) 0xFFFF;
    private final static int    MAXBITS    = 12;
    private final static int    MAXSTR     = (1 << MAXBITS);
    private final static short  HASHSIZE   = 9973;
    private final static short  HASHSTEP   = 2039;

    private byte   strChr_[];
    private short  strNxt_[];
    private short  strHsh_[];
    private short  nStrings_; 


  
    LzwStringTable()
    {
      strChr_ = new byte[MAXSTR];
      strNxt_ = new short[MAXSTR];
      strHsh_ = new short[HASHSIZE];
    }

  
    int addCharString (short index, byte b)
    {
      int hshidx;
      if ( nStrings_ >= MAXSTR )
        return 0xFFFF;

      hshidx = hash (index, b);
      while ( strHsh_[hshidx] != HASH_FREE )
        hshidx = (hshidx + HASHSTEP) % HASHSIZE;

      strHsh_[hshidx] = nStrings_;
      strChr_[nStrings_] = b;
      strNxt_[nStrings_] = (index != HASH_FREE)?index:NEXT_FIRST;

      return nStrings_++;
    }



    short findCharString(short index, byte b)
    {
      int hshidx, nxtidx;

      if ( index == HASH_FREE )
        return b;

      hshidx = hash (index, b);
      while ( (nxtidx = strHsh_[hshidx]) != HASH_FREE ) {
        if ( strNxt_[nxtidx] == index && strChr_[nxtidx] == b )
          return(short) nxtidx;
        hshidx = (hshidx + HASHSTEP) % HASHSIZE;
      }

      return(short) 0xFFFF;
    }


  
    void clearTable (int codesize)
    {
      nStrings_ = 0;

      for ( int q = 0; q < HASHSIZE; q ++ )
        strHsh_[q] = HASH_FREE;

      int w = (1 << codesize) + RES_CODES;
      for ( int q = 0; q < w; q ++ )
        this.addCharString((short) 0xFFFF, (byte) q);
    }


  
    int hash (short index, byte lastbyte)
    {
      return ((int)((short) (lastbyte << 8) ^ index) & 0xFFFF) % HASHSIZE;
    }
  }



  private class BitFile
  {
    private OutputStream stream_ = null;
    private byte[]       buffer_;
    private int          streamIndex_, bitsLeft_;


  
    BitFile(OutputStream stream)
    {
      stream_      = stream;
      buffer_      = new byte[256];
      streamIndex_ = 0;
      bitsLeft_    = 8;
    }


  
    void flush()
      throws IOException
    {
      int nBytes = streamIndex_ + ((bitsLeft_ == 8) ? 0 : 1);
                                                  
      if (nBytes > 0) {
        stream_.write (nBytes);
        stream_.write (buffer_, 0, nBytes);

        buffer_[0]   = 0;
        streamIndex_ = 0;
        bitsLeft_    = 8;
      }
    }


  
    void writeBits (int bits, int nBits)
      throws IOException
    {
      int nBitsWritten = 0;
      int nBytes       = 255;

      do {
        if ((streamIndex_ == 254 && bitsLeft_ == 0) || streamIndex_ > 254) {
          stream_.write (nBytes);
          stream_.write (buffer_, 0, nBytes);
        
          buffer_[0]   = 0;
          streamIndex_ = 0;
          bitsLeft_    = 8;
        }
      
        if (nBits <= bitsLeft_) {
          buffer_[streamIndex_] |= (bits & ((1 << nBits) - 1)) << (8 - bitsLeft_);
                                                             
          nBitsWritten += nBits;
          bitsLeft_    -= nBits;
          nBits         = 0;
        }

        else {
          buffer_[streamIndex_] |= (bits & ((1 << bitsLeft_) - 1)) <<
                                   (8 - bitsLeft_);
                                                             
          nBitsWritten += bitsLeft_;
          bits >>= bitsLeft_;
          nBits -= bitsLeft_;
          buffer_[++streamIndex_] = 0;
          bitsLeft_ = 8;
        }
      
      } while (nBits != 0);
    }
  }
}