/* $RCSfile$
 * $Author: hansonr $
 * $Date: 2007-06-02 12:14:13 -0500 (Sat, 02 Jun 2007) $
 * $Revision: 7831 $
 *
 * Copyright (C) 2000-2005  The Jmol Development Team
 *
 * Contact: jmol-developers@lists.sf.net
 *
 *  This library 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 library 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 library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 */

//  Final encoding code from http://acme.com/resources/classes/Acme/JPM/Encoders/GifEncoder.java
//
//  GifEncoder - write out an image as a GIF
// 
// 
//  Transparency handling and variable bit size courtesy of Jack Palevich.
//  
//  Copyright (C)1996,1998 by Jef Poskanzer <jef@mail.acme.com>. All rights reserved.
//  
//  Redistribution and use in source and binary forms, with or without
//  modification, are permitted provided that the following conditions
//  are met:
//  1. Redistributions of source code must retain the above copyright
//     notice, this list of conditions and the following disclaimer.
//  2. Redistributions in binary form must reproduce the above copyright
//     notice, this list of conditions and the following disclaimer in the
//     documentation and/or other materials provided with the distribution.
// 
//  THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
//  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
//  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
//  ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
//  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
//  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
//  OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
//  HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
//  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
//  OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
//  SUCH DAMAGE.
// 
//  Visit the ACME Labs Java page for up-to-date versions of this and other
//  fine Java utilities: http://www.acme.com/java/
// 
/// Write out an image as a GIF.
// <P>
// <A HREF="/resources/classes/Acme/JPM/Encoders/GifEncoder.java">Fetch the software.</A><BR>
// <A HREF="/resources/classes/Acme.tar.gz">Fetch the entire Acme package.</A>
// <P>
// @see ToGif

package javajs.img;

import javajs.util.CU;
import javajs.util.Lst;
import javajs.util.M3;
import javajs.util.P3;

import java.util.Hashtable;
import java.util.Map;
import java.io.IOException;

/**
 * 
 * GifEncoder extensively adapted for Jmol by Bob Hanson
 * 
 * Color quantization roughly follows the GIMP method
 * "dither Floyd-Steinberg (normal)" but with some twists. (For example, we
 * exclude the background color.)
 * 
 * Note that although GIMP code annotation refers to "median-cut", it is really
 * using MEAN-cut. That is what I use here as well.
 * 
 * -- commented code allows visualization of the color space using Jmol. Very
 * enlightening!
 * 
 * -- much simplified interface with ImageEncoder
 * 
 * -- uses simple Hashtable with Integer() to catalog colors
 * 
 * -- allows progressive production of animated GIF via Jmol CAPTURE command
 * 
 * -- uses general purpose javajs.util.OutputChannel for byte-handling options
 * such as posting to a server, writing to disk, and retrieving bytes.
 * 
 * -- allows JavaScript port
 * 
 * -- Bob Hanson, first try: 24 Sep 2013; final coding: 9 Nov 2014
 * 
 * 
 * @author Bob Hanson hansonr@stolaf.edu
 */

public class GifEncoder extends ImageEncoder {

  private Map<String, Object> params;
  private P3[] palette;
  private int backgroundColor;

  private boolean interlaced;
  private boolean addHeader = true;
  private boolean addImage = true;
  private boolean addTrailer = true;
  private boolean isTransparent;
  private boolean floydSteinberg = true;
  private boolean capturing;
  private boolean looping;

  private int delayTime100ths = -1;
  private int bitsPerPixel = 1;

  private int byteCount;
  private int loopCount; // indefinite loop
  private boolean isDelay;

  /**
   * we allow for animated GIF by being able to re-enter the code with different
   * parameters held in params
   * 
   * 
   */
  @Override
  protected void setParams(Map<String, Object> params) {
    this.params = params;
    Integer ic = (Integer) params.get("transparentColor");
    if (ic == null) {
      ic = (Integer) params.get("backgroundColor");
      if (ic != null) 
        backgroundColor = ic.intValue();
    } else {
      backgroundColor = ic.intValue();
      isTransparent = true;
    }

    if (backgroundColor == 0xFF000000) {
      // fix speckling of text Jmol 14.32.69
      // must return background to true color
      for (int i = pixels.length; --i >= 0;)
          pixels[i] = pixels[i] &~0x040404;
    }

    interlaced = (Boolean.TRUE == params.get("interlaced"));
    if (params.containsKey("captureRootExt") // file0000.gif 
        || !params.containsKey("captureMode")) // animated gif
      return;
    interlaced = false;
    capturing = true;
    Integer c = (Integer) params.get("captureByteCount");
    if (c != null)
      byteCount = c.intValue();
    switch ("maec"
        .indexOf(((String) params.get("captureMode")).substring(0, 1))) {
    case 0: //"movie"
      params.put("captureMode", "add");
      addImage = false;
      addTrailer = false;
      break;
    case 1: // add 
      addHeader = false;
      addTrailer = false;
      Integer msDelay = (Integer) params.get("captureDelayMS");
      if (msDelay == null) {
        int fps = Math.abs(((Integer) params.get("captureFps")).intValue());
        delayTime100ths = (fps == 0 ? 0 : 100 / fps);
      } else {
        isDelay = true;
        // one-time frame delay
        delayTime100ths = msDelay.intValue() / 10;
        params.remove("captureDelayMS");
      }
      looping = (Boolean.FALSE != params.get("captureLooping"));
      loopCount = (looping ? 0 : 1);
      break;
    case 2: // end
      addHeader = false;
      addImage = false;
      break;
    case 3: // cancel
      addHeader = false;
      addImage = false;
      out.cancel();
      break;
    }
  }

  @Override
  protected void generate() throws IOException {
    if (addHeader)
      writeHeader();
    addHeader = false; // only one header
    if (addImage) {
      createPalette();
      writeGraphicControlExtension();
      if (delayTime100ths >= 0 && (looping || isDelay))
        writeNetscapeLoopExtension();
      writeImage();
    }
  }

  @Override
  protected void close() {
    if (addTrailer) {
      writeTrailer();
    } else {
      doClose = false;
    }
    if (capturing)
      params.put("captureByteCount", Integer.valueOf(byteCount));
  }

  //////////////  256-color quantization  //////////////

  /**
   * a color point in normalized L*a*b space with a flag indicating whether it
   * is the background color
   */
  private class ColorItem extends P3 {
    protected boolean isBackground;

    ColorItem(int rgb, boolean isBackground) {
      this.isBackground = isBackground;
      setT(toLABnorm(rgb));
    }
  }

  /**
   * A list of normalized L*a*b points with an index and a center and volume
   * 
   */
  private class ColorCell extends Lst<P3> {

    protected int index;
    protected P3 center;

    private float volume;

    ColorCell(int index) {
      this.index = index;
    }

    /**
     * @param doVisualize
     *        debugging only
     * @return volume in normalized L*a*b space
     */
    public float getVolume(boolean doVisualize) {
      if (volume != 0)
        return volume;
      if (size() < 2)
        return -1;
      //if (true)
      //return lst.size();
      //float d;
      float maxx = -Integer.MAX_VALUE;
      float minx = Integer.MAX_VALUE;
      float maxy = -Integer.MAX_VALUE;
      float miny = Integer.MAX_VALUE;
      float maxz = -Integer.MAX_VALUE;
      float minz = Integer.MAX_VALUE;
      int n = size();
      for (int i = n; --i >= 0;) {
        P3 xyz = get(i);
        if (xyz.x < minx)
          minx = xyz.x;
        if (xyz.y < miny)
          miny = xyz.y;
        if (xyz.z < minz)
          minz = xyz.z;
        if (xyz.x > maxx)
          maxx = xyz.x;
        if (xyz.y > maxy)
          maxy = xyz.y;
        if (xyz.z > maxz)
          maxz = xyz.z;
      }
      float dx = (maxx - minx);
      float dy = (maxy - miny);
      float dz = (maxz - minz);
      // Jmol visualization only
      //      if (doVisualize) {
      //        P3 ptRGB = toRGB(center);
      //        drawPt(index, -size(), ptRGB);
      //        //for (int i = n; --i >= 0;)
      //        //drawPt(index, i, toRGB(get(i)));
      //        P3 pt0 = toRGB(P3.new3(Math.max(minx, 0), Math.max(miny, 0),
      //            Math.max(minz, 0)));
      //        P3 pt1 = toRGB(P3.new3(Math.min(maxx, 100), Math.min(maxy, 100),
      //            Math.min(maxz, 100)));
      //        rgbToXyz(pt0, pt0);
      //        xyzToLab(pt0, pt0);
      //        rgbToXyz(pt1, pt1);
      //        xyzToLab(pt1, pt1);
      //        System.out.println("boundbox corners " + pt0 + " " + pt1);
      //        System.out.println("draw d" + index + " boundbox color " + ptRGB
      //            + " mesh nofill");
      //      }
      return volume = dx * dx + dy * dy + dz * dz;
    }

    //    // Jmol visualization only
    //      private void drawPt(int index, int i, P3 rgb) {
    //        boolean isMain = (i < 0);
    //      P3 lab = rgbToXyz(rgb, null);
    //      xyzToLab(lab, lab);
    //      System.out.println("draw d" + index + (isMain ? "_" : "_" + i) + " width "
    //          + (isMain ? 1.0 : 0.2) + " " + lab
    //          + " color " + rgb + (isMain ? " '" + -i + "'" : ""));
    //      }

    /**
     * Set the average normalized L*a*b value for this cell and return its RGB point
     * 
     * @return RGB point
     * 
     */
    protected P3 setColor() {
      int count = size();
      center = new P3();
      for (int i = count; --i >= 0;)
        center.add(get(i));
      center.scale(1f / count);
      // Jmol visualization only
      //volume = 0;
      //getVolume(true); 
      return toRGB(center);
    }

    /**
     * use median_cut algorithm to split the cell, creating a doubly linked
     * list.
     * 
     * Paul Heckbert, MIT thesis COLOR IMAGE QUANTIZATION FOR FRAME BUFFER
     * DISPLAY https://www.cs.cmu.edu/~ph/ciq_thesis
     * 
     * except, as in GIMP, we use center (not median) here.
     * 
     * @param cells
     * @return true if split
     */
    protected boolean splitCell(Lst<ColorCell> cells) {
      int n = size();
      if (n < 2)
        return false;
      int newIndex = cells.size();
      ColorCell newCell = new ColorCell(newIndex);
      cells.addLast(newCell);
      float[][] ranges = new float[3][3];
      for (int ic = 0; ic < 3; ic++) {
        float low = Float.MAX_VALUE;
        float high = -Float.MAX_VALUE;
        for (int i = n; --i >= 0;) {
          P3 lab = get(i);
          float v = (ic == 0 ? lab.x : ic == 1 ? lab.y : lab.z);
          if (low > v)
            low = v;
          if (high < v)
            high = v;
        }
        ranges[0][ic] = low;
        ranges[1][ic] = high;
        ranges[2][ic] = high - low;
      }
      float[] r = ranges[2];
      int mode = (r[0] >= r[1] ? (r[0] >= r[2] ? 0 : 2) : r[1] >= r[2] ? 1 : 2);
      float val = ranges[0][mode] + ranges[2][mode] / 2;
      volume = 0; // recalculate volume if needed
      switch (mode) {
      case 0:
        for (int i = n; --i >= 0;)
          if (get(i).x >= val)
            newCell.addLast(removeItemAt(i));
        break;
      case 1:
        for (int i = n; --i >= 0;)
          if (get(i).y >= val)
            newCell.addLast(removeItemAt(i));
        break;
      case 2:
        for (int i = size(); --i >= 0;)
          if (get(i).z >= val)
            newCell.addLast(removeItemAt(i));
        break;
      }
      return true;
    }
  }

  /**
   * Quantize all colors and create the final palette;
   * replace pixels[] with an array of color indices.
   * 
   */
  private void createPalette() {

    // catalog all pixel colors

    Lst<ColorItem> tempColors = new Lst<ColorItem>();
    Map<Integer, ColorItem> ciHash = new Hashtable<Integer, ColorItem>();
    for (int i = 0, n = pixels.length; i < n; i++) {
      int rgb = pixels[i];
      Integer key = Integer.valueOf(rgb);
      ColorItem item = ciHash.get(key);
      if (item == null) {
        item = new ColorItem(rgb, rgb == backgroundColor);
        ciHash.put(key, item);
        tempColors.addLast(item);
      }
    }
    int nColors = tempColors.size();
    System.out.println("GIF total image colors: " + nColors);
    ciHash = null;

    // create a set of <= 256 color cells

    Lst<ColorCell> cells = quantizeColors(tempColors);
    nColors = cells.size();
    System.out.println("GIF final color count: " + nColors);

    // generate the palette and map each cell's rgb color to itself

    Map<Integer, ColorCell> colorMap = new Hashtable<Integer, ColorCell>();
    bitsPerPixel = (nColors <= 2 ? 1 : nColors <= 4 ? 2 : nColors <= 16 ? 4 : 8);
    palette = new P3[1 << bitsPerPixel];
    for (int i = 0; i < nColors; i++) {
      ColorCell c = cells.get(i);
      colorMap.put(
          Integer.valueOf(CU.colorPtToFFRGB(palette[i] = c.setColor())), c);
    }

    // index all pixels to a pallete color

    pixels = indexPixels(cells, colorMap);
  }

  /**
   * Quantize colors by generating a set of cells in normalized L*a*b space
   * containing all colors. Start with just two cells -- fixed background color
   * and all others. Keep splitting cells while there are fewer than 256 and
   * some with multiple colors in them.
   * 
   * It is possible that we will end up with fewer than 256 colors.
   * 
   * @param tempColors
   * @return final list of colors
   */
  private Lst<ColorCell> quantizeColors(Lst<ColorItem> tempColors) {
    int n = tempColors.size();
    Lst<ColorCell> cells = new Lst<ColorCell>();
    ColorCell cc = new ColorCell(0);
    cc.addLast(new ColorItem(backgroundColor, true));
    cells.addLast(cc);
    cc = new ColorCell(1);
    if (n > 256)
      cells.addLast(cc);
    for (int i = 0; i < n; i++) {
      ColorItem c = tempColors.get(i);
      if (c.isBackground)
        continue;
      cc.addLast(c);
      if (n <= 256) {
        cells.addLast(cc);
        cc = new ColorCell(cells.size());
      }
    }
    tempColors.clear();
    if (n > 256)
      while ((n = cells.size()) < 256) {
        float maxVol = 0;
        ColorCell maxCell = null;
        for (int i = n; --i >= 1;) {
          ColorCell c = cells.get(i);
          float v = c.getVolume(false);
          if (v > maxVol) {
            maxVol = v;
            maxCell = c;
          }
        }
        if (maxCell == null || !maxCell.splitCell(cells))
          break;
      }
    return cells;
  }

  /**
   * 
   * Assign all colors to their closest approximation and return an array of
   * color indexes.
   * 
   * Uses Floyd-Steinberg dithering, finding the closest known color and then
   * spreading out the error over four leading pixels. Limits error to +/- 75
   * percent in normalized L*a*b space.
   * 
   * @param cells
   *        quantized color cells
   * @param colorMap
   *        map of quantized rgb to its cell
   * @return array of color indexes, one for each pixel
   * 
   */
  private int[] indexPixels(Lst<ColorCell> cells,
                            Map<Integer, ColorCell> colorMap) {
    // We need a strip only width+2 wide to process all the errors.
    // Errors are added to the next pixel and the next row's pixels 
    // only through p + width + 1:
    //         p  +1
    //   +w-1 +w  +w+1
    // so including p as well, we need a total of width + 2 errors.
    //
    // as p moves through the pixels, we just use mod to cycle through
    // this strip.
    //
    int w2 = width + 2;
    P3[] errors = new P3[w2];
    // We should replace, not overwrite, pixels 
    // as this may be the raw canvas.buf32.
    int[] newPixels = new int[pixels.length];
    P3 err = new P3();
    P3 lab;
    int rgb;
    Map<Integer, ColorCell> nearestCell = new Hashtable<Integer, ColorCell>();
    for (int i = 0, p = 0; i < height; ++i) {
      boolean notLastRow = (i != height - 1);
      for (int j = 0; j < width; ++j, p++) {
        if (pixels[p] == backgroundColor) {
          // leave as 0
          continue;
        }
        P3 pe = errors[p % w2];
        if (pe == null || pe.x == Float.MAX_VALUE) {
          lab = null;
          rgb = pixels[p];
        } else {
          lab = toLABnorm(pixels[p]);
          err = pe;
          // important not to round the clamp here -- full floating precision
          err.x = clamp(err.x, -75, 75);
          err.y = clamp(err.y, -75, 75);
          err.z = clamp(err.z, -75, 75);
          lab.add(err);
          rgb = CU.colorPtToFFRGB(toRGB(lab));
        }
        Integer key = Integer.valueOf(rgb);
        ColorCell cell = colorMap.get(key);
        if (cell == null) {
          // critical to generate normalized L*a*b from RGB here for nearestCell mapping.
          // otherwise future RGB keys may match the wrong cell
          lab = toLABnorm(rgb);
          cell = nearestCell.get(key);
          if (cell == null) {
            // find nearest cell
            float maxerr = Float.MAX_VALUE;
            // skip 0 0 0
            for (int ib = cells.size(); --ib >= 1;) {
              ColorCell c = cells.get(ib);
              err.sub2(lab, c.center);
              float d = err.lengthSquared();
              if (d < maxerr) {
                maxerr = d;
                cell = c;
              }
            }
            nearestCell.put(key, cell);
          }
          if (floydSteinberg) {
            // dither
            err.sub2(lab, cell.center);
            boolean notLastCol = (j < width - 1);
            if (notLastCol)
              addError(err, 7, errors, p + 1, w2);
            if (notLastRow) {
              if (j > 0)
                addError(err, 3, errors, p + width - 1, w2);
              addError(err, 5, errors, p + width, w2);
              if (notLastCol)
                addError(err, 1, errors, p + width + 1, w2);
            }
          }
          err.x = Float.MAX_VALUE; // used; flag for resetting to 0
        }
        newPixels[p] = cell.index;
      }
    }
    return newPixels;
  }

  private void addError(P3 err, int f, P3[] errors, int p, int w2) {
    // GIMP will allow changing the background color.
    if (pixels[p] == backgroundColor)
      return;
    p %= w2;
    P3 errp = errors[p];
    if (errp == null)
      errp = errors[p] = new P3();
    else if (errp.x == Float.MAX_VALUE) // reuse
      errp.set(0, 0, 0);
    errp.scaleAdd2(f / 16f, err, errp);
  }

  ///////////////////////// CIE L*a*b / XYZ / sRGB conversion methods /////////

  // these could be static, but that just makes for more JavaScript code

  protected P3 toLABnorm(int rgb) {
    P3 lab = CU.colorPtFromInt(rgb, null);
    rgbToXyz(lab, lab);
    xyzToLab(lab, lab);
    // normalize to 0-100
    lab.y = (lab.y + 86.185f) / (98.254f + 86.185f) * 100f;
    lab.z = (lab.z + 107.863f) / (94.482f + 107.863f) * 100f;
    return lab;
  }

  protected P3 toRGB(P3 lab) {
    P3 xyz = P3.newP(lab);
    // normalized to 0-100
    xyz.y = xyz.y / 100f * (98.254f + 86.185f) - 86.185f;
    xyz.z = xyz.z / 100f * (94.482f + 107.863f) - 107.863f;
    labToXyz(xyz, xyz);
    return xyzToRgb(xyz, xyz);
  }

  private static M3 xyz2rgb;
  private static M3 rgb2xyz;

  static {
    rgb2xyz = M3.newA9(new float[] { 0.4124f, 0.3576f, 0.1805f, 0.2126f,
        0.7152f, 0.0722f, 0.0193f, 0.1192f, 0.9505f });

    xyz2rgb = M3.newA9(new float[] { 3.2406f, -1.5372f, -0.4986f, -0.9689f,
        1.8758f, 0.0415f, 0.0557f, -0.2040f, 1.0570f });
  }

  public P3 rgbToXyz(P3 rgb, P3 xyz) {
    // http://en.wikipedia.org/wiki/CIE_1931_color_space
    // http://rsb.info.nih.gov/ij/plugins/download/Color_Space_Converter.java
    if (xyz == null)
      xyz = new P3();
    xyz.x = sxyz(rgb.x);
    xyz.y = sxyz(rgb.y);
    xyz.z = sxyz(rgb.z);
    rgb2xyz.rotate(xyz);
    return xyz;
  }

  private float sxyz(float x) {
    x /= 255;
    return (float) (x <= 0.04045 ? x / 12.92 : Math.pow(((x + 0.055) / 1.055),
        2.4)) * 100;
  }

  public P3 xyzToRgb(P3 xyz, P3 rgb) {
    // http://en.wikipedia.org/wiki/CIE_1931_color_space
    // http://rsb.info.nih.gov/ij/plugins/download/Color_Space_Converter.java
    if (rgb == null)
      rgb = new P3();
    rgb.setT(xyz);
    rgb.scale(0.01f);
    xyz2rgb.rotate(rgb);
    rgb.x = clamp(srgb(rgb.x), 0, 255);
    rgb.y = clamp(srgb(rgb.y), 0, 255);
    rgb.z = clamp(srgb(rgb.z), 0, 255);
    return rgb;
  }

  private float srgb(float x) {
    return (float) (x > 0.0031308f ? (1.055 * Math.pow(x, 1.0 / 2.4)) - 0.055
        : x * 12.92) * 255;
  }

  public P3 xyzToLab(P3 xyz, P3 lab) {
    // http://en.wikipedia.org/wiki/Lab_color_space
    // http://rsb.info.nih.gov/ij/plugins/download/Color_Space_Converter.java
    // Lab([0..100], [-86.185..98.254], [-107.863..94.482])
    // XYZn = D65 = {95.0429, 100.0, 108.8900};
    if (lab == null)
      lab = new P3();
    float x = flab(xyz.x / 95.0429f);
    float y = flab(xyz.y / 100);
    float z = flab(xyz.z / 108.89f);
    lab.x = (116 * y) - 16;
    lab.y = 500 * (x - y);
    lab.z = 200 * (y - z);
    return lab;
  }

  private float flab(float t) {
    return (float) (t > 8.85645168E-3 /* (24/116)^3 */? Math.pow(t,
        0.333333333) : 7.78703704 /* 1/3*116/24*116/24 */* t + 0.137931034 /* 16/116 */
    );
  }

  public P3 labToXyz(P3 lab, P3 xyz) {
    // http://en.wikipedia.org/wiki/Lab_color_space
    // http://rsb.info.nih.gov/ij/plugins/download/Color_Space_Converter.java
    // XYZn = D65 = {95.0429, 100.0, 108.8900};
    if (xyz == null)
      xyz = new P3();

    xyz.setT(lab);
    float y = (xyz.x + 16) / 116;
    float x = xyz.y / 500 + y;
    float z = y - xyz.z / 200;
    xyz.x = fxyz(x) * 95.0429f;
    xyz.y = fxyz(y) * 100;
    xyz.z = fxyz(z) * 108.89f;

    return xyz;
  }

  private float fxyz(float t) {
    return (float) (t > 0.206896552 /* (24/116) */? t * t * t
        : 0.128418549 /* 3*24/116*24/116 */* (t - 0.137931034 /* 16/116 */));
  }

  private float clamp(float c, float min, float max) {
    c = (c < min ? min : c > max ? max : c);
    return (min == 0 ? Math.round(c) : c);
  }

  ///////////////////////// GifEncoder writing methods ////////////////////////

  /**
   * includes logical screen descriptor
   * 
   * @throws IOException
   */
  private void writeHeader() throws IOException {
    putString("GIF89a");
    putWord(width);
    putWord(height);
    putByte(0); // no global color table -- using local instead
    putByte(0); // no background
    putByte(0); // no pixel aspect ratio given
  }

  private void writeGraphicControlExtension() {
    if (isTransparent || delayTime100ths >= 0) {
      putByte(0x21); // graphic control extension
      putByte(0xf9); // graphic control label
      putByte(4); // block size
      putByte((isTransparent ? 9 : 0) | (delayTime100ths > 0 ? 2 : 0)); // packed bytes 
      putWord(delayTime100ths > 0 ? delayTime100ths : 0);
      putByte(0); // transparent index
      putByte(0); // end-of-block
    }
  }

  // see  http://www.vurdalakov.net/misc/gif/netscape-looping-application-extension
  //      +---------------+
  //   0  |     0x21      |  Extension Label
  //      +---------------+
  //   1  |     0xFF      |  Application Extension Label
  //      +---------------+
  //   2  |     0x0B      |  Block Size
  //      +---------------+
  //   3  |               | 
  //      +-             -+
  //   4  |               | 
  //      +-             -+
  //   5  |               | 
  //      +-             -+
  //   6  |               | 
  //      +-  NETSCAPE   -+  Application Identifier (8 bytes)
  //   7  |               | 
  //      +-             -+
  //   8  |               | 
  //      +-             -+
  //   9  |               | 
  //      +-             -+
  //  10  |               | 
  //      +---------------+
  //  11  |               | 
  //      +-             -+
  //  12  |      2.0      |  Application Authentication Code (3 bytes)
  //      +-             -+
  //  13  |               | 
  //      +===============+                      --+
  //  14  |     0x03      |  Sub-block Data Size   |
  //      +---------------+                        |
  //  15  |     0x01      |  Sub-block ID          |
  //      +---------------+                        | Application Data Sub-block
  //  16  |               |                        |
  //      +-             -+  Loop Count (2 bytes)  |
  //  17  |               |                        |
  //      +===============+                      --+
  //  18  |     0x00      |  Block Terminator
  //      +---------------+

  private void writeNetscapeLoopExtension() {
    putByte(0x21); // graphic control extension
    putByte(0xff); // netscape loop extension
    putByte(0x0B); // block size
    putString("NETSCAPE2.0");
    putByte(3);
    putByte(1);
    putWord(loopCount);
    putByte(0); // end-of-block

  }

  private int initCodeSize;
  private int curpt;

  private void writeImage() {
    putByte(0x2C);
    putWord(0); //left
    putWord(0); //top
    putWord(width);
    putWord(height);

    //    <Packed Fields>  =      LISx xZZZ

    //    L Local Color Table Flag
    //    I Interlace Flag
    //    S Sort Flag
    //    x Reserved
    //    ZZZ Size of Local Color Table

    int packedFields = 0x80 | (interlaced ? 0x40 : 0) | (bitsPerPixel - 1);
    putByte(packedFields);
    int colorMapSize = 1 << bitsPerPixel;
    P3 p = new P3();
    for (int i = 0; i < colorMapSize; i++) {
      if (palette[i] != null)
        p = palette[i];
      putByte((int) p.x);
      putByte((int) p.y);
      putByte((int) p.z);
    }
    putByte(initCodeSize = (bitsPerPixel <= 1 ? 2 : bitsPerPixel));
    compress();
    putByte(0);
  }

  private void writeTrailer() {
    // Write the GIF file terminator
    putByte(0x3B);
  }

  ///// compression routines /////

  private static final int EOF = -1;

  // Return the next pixel from the image
  private int nextPixel() {
    if (countDown-- == 0)
      return EOF;
    int colorIndex = pixels[curpt];
    // Bump the current X position
    ++curx;
    if (curx == width) {
      // If we are at the end of a scan line, set curx back to the beginning
      // If we are interlaced, bump the cury to the appropriate spot,
      // otherwise, just increment it.
      curx = 0;
      if (interlaced)
        updateY(INTERLACE_PARAMS[pass], INTERLACE_PARAMS[pass + 4]);
      else
        ++cury;
    }
    curpt = cury * width + curx;
    return colorIndex & 0xff;
  }

  private static final int[] INTERLACE_PARAMS = { 8, 8, 4, 2, 4, 2, 1, 0 };

  /**
   * 
   * Group 1 : Every 8th. row, starting with row 0. (Pass 1)
   * 
   * Group 2 : Every 8th. row, starting with row 4. (Pass 2)
   * 
   * Group 3 : Every 4th. row, starting with row 2. (Pass 3)
   * 
   * Group 4 : Every 2nd. row, starting with row 1. (Pass 4)
   * 
   * @param yNext
   * @param yNew
   */
  private void updateY(int yNext, int yNew) {
    cury += yNext;
    if (yNew >= 0 && cury >= height) {
      cury = yNew;
      ++pass;
    }
  }

  // Write out a word to the GIF file
  private void putWord(int w) {
    putByte(w);
    putByte(w >> 8);
  }

  // GIFCOMPR.C       - GIF Image compression routines
  //
  // Lempel-Ziv compression based on 'compress'.  GIF modifications by
  // David Rowley (mgardi@watdcsu.waterloo.edu)

  // General DEFINEs

  private static final int BITS = 12;

  private static final int HSIZE = 5003; // 80% occupancy

  // GIF Image compression - modified 'compress'
  //
  // Based on: compress.c - File compression ala IEEE Computer, June 1984.
  //
  // By Authors:  Spencer W. Thomas      (decvax!harpo!utah-cs!utah-gr!thomas)
  //              Jim McKie              (decvax!mcvax!jim)
  //              Steve Davies           (decvax!vax135!petsd!peora!srd)
  //              Ken Turkowski          (decvax!decwrl!turtlevax!ken)
  //              James A. Woods         (decvax!ihnp4!ames!jaw)
  //              Joe Orost              (decvax!vax135!petsd!joe)

  private int nBits; // number of bits/code
  private int maxbits = BITS; // user settable max # bits/code
  private int maxcode; // maximum code, given n_bits
  private int maxmaxcode = 1 << BITS; // should NEVER generate this code

  private final static int MAXCODE(int nBits) {
    return (1 << nBits) - 1;
  }

  private int[] htab = new int[HSIZE];
  private int[] codetab = new int[HSIZE];

  private int hsize = HSIZE; // for dynamic table sizing

  private int freeEnt = 0; // first unused entry

  // block compression parameters -- after all codes are used up,
  // and compression rate changes, start over.
  private boolean clearFlag = false;

  // Algorithm:  use open addressing double hashing (no chaining) on the
  // prefix code / next character combination.  We do a variant of Knuth's
  // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime
  // secondary probe.  Here, the modular division first probe is gives way
  // to a faster exclusive-or manipulation.  Also do block compression with
  // an adaptive reset, whereby the code table is cleared when the compression
  // ratio decreases, but after the table fills.  The variable-length output
  // codes are re-sized at this point, and a special CLEAR code is generated
  // for the decompressor.  Late addition:  construct the table according to
  // file size for noticeable speed improvement on small files.  Please direct
  // questions about this implementation to ames!jaw.

  private int clearCode;
  private int EOFCode;

  private int countDown;
  private int pass = 0;
  private int curx, cury;

  private void compress() {

    // Calculate number of bits we are expecting
    countDown = width * height;

    // Indicate which pass we are on (if interlace)
    pass = 0;
    // Set up the current x and y position
    curx = 0;
    cury = 0;

    // Set up the necessary values
    clearFlag = false;
    nBits = initCodeSize + 1;
    maxcode = MAXCODE(nBits);

    clearCode = 1 << initCodeSize;
    EOFCode = clearCode + 1;
    freeEnt = clearCode + 2;

    // Set up the 'byte output' routine
    bufPt = 0;

    int ent = nextPixel();

    int hshift = 0;
    int fcode;
    for (fcode = hsize; fcode < 65536; fcode *= 2)
      ++hshift;
    hshift = 8 - hshift; // set hash code range bound

    int hsizeReg = hsize;
    clearHash(hsizeReg); // clear hash table

    output(clearCode);

    int c;
    outer_loop: while ((c = nextPixel()) != EOF) {
      fcode = (c << maxbits) + ent;
      int i = (c << hshift) ^ ent; // xor hashing

      if (htab[i] == fcode) {
        ent = codetab[i];
        continue;
      } else if (htab[i] >= 0) // non-empty slot
      {
        int disp = hsizeReg - i; // secondary hash (after G. Knott)
        if (i == 0)
          disp = 1;
        do {
          if ((i -= disp) < 0)
            i += hsizeReg;

          if (htab[i] == fcode) {
            ent = codetab[i];
            continue outer_loop;
          }
        } while (htab[i] >= 0);
      }
      output(ent);
      ent = c;
      if (freeEnt < maxmaxcode) {
        codetab[i] = freeEnt++; // code -> hashtable
        htab[i] = fcode;
      } else {
        clearBlock();
      }
    }
    // Put out the final code.
    output(ent);
    output(EOFCode);
  }

  // output
  //
  // Output the given code.
  // Inputs:
  //      code:   A n_bits-bit integer.  If == -1, then EOF.  This assumes
  //              that n_bits =< wordsize - 1.
  // Outputs:
  //      Outputs code to the file.
  // Assumptions:
  //      Chars are 8 bits long.
  // Algorithm:
  //      Maintain a BITS character long buffer (so that 8 codes will
  // fit in it exactly).  Use the VAX insv instruction to insert each
  // code in turn.  When the buffer fills up empty it and start over.

  private int curAccum = 0;
  private int curBits = 0;

  private int masks[] = { 0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F,
      0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF,
      0x7FFF, 0xFFFF };

  private void output(int code) {
    curAccum &= masks[curBits];

    if (curBits > 0)
      curAccum |= (code << curBits);
    else
      curAccum = code;

    curBits += nBits;

    while (curBits >= 8) {
      byteOut((byte) (curAccum & 0xff));
      curAccum >>= 8;
      curBits -= 8;
    }

    // If the next entry is going to be too big for the code size,
    // then increase it, if possible.
    if (freeEnt > maxcode || clearFlag) {
      if (clearFlag) {
        maxcode = MAXCODE(nBits = initCodeSize + 1);
        clearFlag = false;
      } else {
        ++nBits;
        if (nBits == maxbits)
          maxcode = maxmaxcode;
        else
          maxcode = MAXCODE(nBits);
      }
    }

    if (code == EOFCode) {
      // At EOF, write the rest of the buffer.
      while (curBits > 0) {
        byteOut((byte) (curAccum & 0xff));
        curAccum >>= 8;
        curBits -= 8;
      }
      flushBytes();
    }
  }

  // Clear out the hash table

  // table clear for block compress
  private void clearBlock() {
    clearHash(hsize);
    freeEnt = clearCode + 2;
    clearFlag = true;

    output(clearCode);
  }

  // reset code table
  private void clearHash(int hsize) {
    for (int i = 0; i < hsize; ++i)
      htab[i] = -1;
  }

  // GIF-specific routines (byte array buffer)

  // Number of bytes so far in this 'packet'
  private int bufPt;

  // Define the storage for the packet accumulator
  final private byte[] buf = new byte[256];

  // Add a byte to the end of the current packet, and if it is 254
  // byte, flush the packet to disk.
  private void byteOut(byte c) {
    buf[bufPt++] = c;
    if (bufPt >= 254)
      flushBytes();
  }

  // Flush the packet to disk, and reset the accumulator
  protected void flushBytes() {
    if (bufPt > 0) {
      putByte(bufPt);
      out.write(buf, 0, bufPt);
      byteCount += bufPt;
      bufPt = 0;
    }
  }

}
