/*
 * Copyright 2000-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */

package org.exoplatform.imageio.plugins.gif;

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.WritableRaster;
import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import javax.imageio.IIOException;
import javax.imageio.ImageReader;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import org.exoplatform.imageio.plugins.common.ReaderUtil;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

/**
 * version: openjdk-7-ea-src-b35-11_sep_2008
 */
public class GIFImageReader extends ImageReader {
  /**
   * Class logger.
   */
  private static final Log LOG                = ExoLogger.getLogger(GIFImageReader.class);

  // The current ImageInputStream source.
  ImageInputStream         stream             = null;

  // Per-stream settings

  // True if the file header including stream metadata has been read.
  boolean                  gotHeader          = false;

  // Global metadata, read once per input setting.
  GIFStreamMetadata        streamMetadata     = null;

  // The current image index
  int                      currIndex          = -1;

  // Metadata for image at 'currIndex', or null.
  GIFImageMetadata         imageMetadata      = null;

  // A List of Longs indicating the stream positions of the
  // start of the metadata for each image. Entries are added
  // as needed.
  List                     imageStartPosition = new ArrayList();

  // Length of metadata for image at 'currIndex', valid only if
  // imageMetadata != null.
  int                      imageMetadataLength;

  // The number of images in the stream, if known, otherwise -1.
  int                      numImages          = -1;

  // Variables used by the LZW decoding process
  byte[]                   block              = new byte[255];

  int                      blockLength        = 0;

  int                      bitPos             = 0;

  int                      nextByte           = 0;

  int                      initCodeSize;

  int                      clearCode;

  int                      eofCode;

  // 32-bit lookahead buffer
  int                      next32Bits         = 0;

  // Try if the end of the data blocks has been found,
  // and we are simply draining the 32-bit buffer
  boolean                  lastBlockFound     = false;

  // The image to be written.
  BufferedImage            theImage           = null;

  // The image's tile.
  WritableRaster           theTile            = null;

  // The image dimensions (from the stream).
  int                      width              = -1, height = -1;

  // The pixel currently being decoded (in the stream's coordinates).
  int                      streamX            = -1, streamY = -1;

  // The number of rows decoded
  int                      rowsDone           = 0;

  // The current interlace pass, starting with 0.
  int                      interlacePass      = 0;

  // End per-stream settings

  // Constants used to control interlacing.
  static final int[]       interlaceIncrement = { 8, 8, 4, 2, -1 };

  static final int[]       interlaceOffset    = { 0, 4, 2, 1, -1 };

  public GIFImageReader(ImageReaderSpi originatingProvider) {
    super(originatingProvider);
  }

  // Take input from an ImageInputStream
  public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) {
    super.setInput(input, seekForwardOnly, ignoreMetadata);
    if (input != null) {
      if (!(input instanceof ImageInputStream)) {
        throw new IllegalArgumentException("input not an ImageInputStream!");
      }
      this.stream = (ImageInputStream) input;
    } else {
      this.stream = null;
    }

    // Clear all values based on the previous stream contents
    resetStreamSettings();
  }

  public int getNumImages(boolean allowSearch) throws IIOException {
    if (stream == null) {
      throw new IllegalStateException("Input not set!");
    }
    if (seekForwardOnly && allowSearch) {
      throw new IllegalStateException("seekForwardOnly and allowSearch can't both be true!");
    }

    if (numImages > 0) {
      return numImages;
    }
    if (allowSearch) {
      this.numImages = locateImage(Integer.MAX_VALUE) + 1;
    }
    return numImages;
  }

  // Throw an IndexOutOfBoundsException if index < minIndex,
  // and bump minIndex if required.
  private void checkIndex(int imageIndex) {
    if (imageIndex < minIndex) {
      throw new IndexOutOfBoundsException("imageIndex < minIndex!");
    }
    if (seekForwardOnly) {
      minIndex = imageIndex;
    }
  }

  public int getWidth(int imageIndex) throws IIOException {
    checkIndex(imageIndex);

    int index = locateImage(imageIndex);
    if (index != imageIndex) {
      throw new IndexOutOfBoundsException();
    }
    readMetadata();
    return imageMetadata.imageWidth;
  }

  public int getHeight(int imageIndex) throws IIOException {
    checkIndex(imageIndex);

    int index = locateImage(imageIndex);
    if (index != imageIndex) {
      throw new IndexOutOfBoundsException();
    }
    readMetadata();
    return imageMetadata.imageHeight;
  }

  public Iterator getImageTypes(int imageIndex) throws IIOException {
    checkIndex(imageIndex);

    int index = locateImage(imageIndex);
    if (index != imageIndex) {
      throw new IndexOutOfBoundsException();
    }
    readMetadata();

    List l = new ArrayList(1);

    byte[] colorTable;
    if (imageMetadata.localColorTable != null) {
      colorTable = imageMetadata.localColorTable;
    } else {
      colorTable = streamMetadata.globalColorTable;
    }

    // Normalize color table length to 2^1, 2^2, 2^4, or 2^8
    int length = colorTable.length / 3;
    int bits;
    if (length == 2) {
      bits = 1;
    } else if (length == 4) {
      bits = 2;
    } else if (length == 8 || length == 16) {
      // Bump from 3 to 4 bits
      bits = 4;
    } else {
      // Bump to 8 bits
      bits = 8;
    }
    int lutLength = 1 << bits;
    byte[] r = new byte[lutLength];
    byte[] g = new byte[lutLength];
    byte[] b = new byte[lutLength];

    // Entries from length + 1 to lutLength - 1 will be 0
    int rgbIndex = 0;
    for (int i = 0; i < length; i++) {
      r[i] = colorTable[rgbIndex++];
      g[i] = colorTable[rgbIndex++];
      b[i] = colorTable[rgbIndex++];
    }

    byte[] a = null;
    if (imageMetadata.transparentColorFlag) {
      a = new byte[lutLength];
      Arrays.fill(a, (byte) 255);

      // Some files erroneously have a transparent color index
      // of 255 even though there are fewer than 256 colors.
      int idx = Math.min(imageMetadata.transparentColorIndex, lutLength - 1);
      a[idx] = (byte) 0;
    }

    int[] bitsPerSample = new int[1];
    bitsPerSample[0] = bits;
    l.add(ImageTypeSpecifier.createIndexed(r, g, b, a, bits, DataBuffer.TYPE_BYTE));
    return l.iterator();
  }

  public ImageReadParam getDefaultReadParam() {
    return new ImageReadParam();
  }

  public IIOMetadata getStreamMetadata() throws IIOException {
    readHeader();
    return streamMetadata;
  }

  public IIOMetadata getImageMetadata(int imageIndex) throws IIOException {
    checkIndex(imageIndex);

    int index = locateImage(imageIndex);
    if (index != imageIndex) {
      throw new IndexOutOfBoundsException("Bad image index!");
    }
    readMetadata();
    return imageMetadata;
  }

  // BEGIN LZW STUFF

  private void initNext32Bits() {
    next32Bits = block[0] & 0xff;
    next32Bits |= (block[1] & 0xff) << 8;
    next32Bits |= (block[2] & 0xff) << 16;
    next32Bits |= block[3] << 24;
    nextByte = 4;
  }

  // Load a block (1-255 bytes) at a time, and maintain
  // a 32-bit lookahead buffer that is filled from the left
  // and extracted from the right.
  //
  // When the last block is found, we continue to
  //
  private int getCode(int codeSize, int codeMask) throws IOException {
    if (bitPos + codeSize > 32) {
      return eofCode; // No more data available
    }

    int code = (next32Bits >> bitPos) & codeMask;
    bitPos += codeSize;

    // Shift in a byte of new data at a time
    while (bitPos >= 8 && !lastBlockFound) {
      next32Bits >>>= 8;
      bitPos -= 8;

      // Check if current block is out of bytes
      if (nextByte >= blockLength) {
        // Get next block size
        blockLength = stream.readUnsignedByte();
        if (blockLength == 0) {
          lastBlockFound = true;
          return code;
        } else {
          int left = blockLength;
          int off = 0;
          while (left > 0) {
            int nbytes = stream.read(block, off, left);
            off += nbytes;
            left -= nbytes;
          }
          nextByte = 0;
        }
      }

      next32Bits |= block[nextByte++] << 24;
    }

    return code;
  }

  public void initializeStringTable(int[] prefix, byte[] suffix, byte[] initial, int[] length) {
    int numEntries = 1 << initCodeSize;
    for (int i = 0; i < numEntries; i++) {
      prefix[i] = -1;
      suffix[i] = (byte) i;
      initial[i] = (byte) i;
      length[i] = 1;
    }

    // Fill in the entire table for robustness against
    // out-of-sequence codes.
    for (int i = numEntries; i < 4096; i++) {
      prefix[i] = -1;
      length[i] = 1;
    }

    // tableIndex = numEntries + 2;
    // codeSize = initCodeSize + 1;
    // codeMask = (1 << codeSize) - 1;
  }

  Rectangle sourceRegion;

  int       sourceXSubsampling;

  int       sourceYSubsampling;

  int       sourceMinProgressivePass;

  int       sourceMaxProgressivePass;

  Point     destinationOffset;

  Rectangle destinationRegion;

  // Used only if IIOReadUpdateListeners are present
  int       updateMinY;

  int       updateYStep;

  boolean   decodeThisRow = true;

  int       destY         = 0;

  byte[]    rowBuf;

  private void outputRow() {
    // Clip against ImageReadParam
    int width = Math.min(sourceRegion.width, destinationRegion.width * sourceXSubsampling);
    int destX = destinationRegion.x;

    if (sourceXSubsampling == 1) {
      theTile.setDataElements(destX, destY, width, 1, rowBuf);
    } else {
      for (int x = 0; x < width; x += sourceXSubsampling, destX++) {
        theTile.setSample(destX, destY, 0, rowBuf[x] & 0xff);
      }
    }

    // Update IIOReadUpdateListeners, if any
    if (updateListeners != null) {
      int[] bands = { 0 };
      // updateYStep will have been initialized if
      // updateListeners is non-null
      processImageUpdate(theImage, destX, destY, width, 1, 1, updateYStep, bands);
    }
  }

  private void computeDecodeThisRow() {
    this.decodeThisRow = (destY < destinationRegion.y + destinationRegion.height)
        && (streamY >= sourceRegion.y) && (streamY < sourceRegion.y + sourceRegion.height)
        && (((streamY - sourceRegion.y) % sourceYSubsampling) == 0);
  }

  private void outputPixels(byte[] string, int len) {
    if (interlacePass < sourceMinProgressivePass || interlacePass > sourceMaxProgressivePass) {
      return;
    }

    for (int i = 0; i < len; i++) {
      if (streamX >= sourceRegion.x) {
        rowBuf[streamX - sourceRegion.x] = string[i];
      }

      // Process end-of-row
      ++streamX;
      if (streamX == width) {
        // Update IIOReadProgressListeners
        ++rowsDone;
        processImageProgress(100.0F * rowsDone / height);

        if (decodeThisRow) {
          outputRow();
        }

        streamX = 0;
        if (imageMetadata.interlaceFlag) {
          streamY += interlaceIncrement[interlacePass];
          if (streamY >= height) {
            // Inform IIOReadUpdateListeners of end of pass
            if (updateListeners != null) {
              processPassComplete(theImage);
            }

            ++interlacePass;
            if (interlacePass > sourceMaxProgressivePass) {
              return;
            }
            streamY = interlaceOffset[interlacePass];
            startPass(interlacePass);
          }
        } else {
          ++streamY;
        }

        // Determine whether pixels from this row will
        // be written to the destination
        this.destY = destinationRegion.y + (streamY - sourceRegion.y) / sourceYSubsampling;
        computeDecodeThisRow();
      }
    }
  }

  // END LZW STUFF

  private void readHeader() throws IIOException {
    if (gotHeader) {
      return;
    }
    if (stream == null) {
      throw new IllegalStateException("Input not set!");
    }

    // Create an object to store the stream metadata
    this.streamMetadata = new GIFStreamMetadata();

    try {
      stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);

      byte[] signature = new byte[6];
      stream.readFully(signature);

      StringBuffer version = new StringBuffer(3);
      version.append((char) signature[3]);
      version.append((char) signature[4]);
      version.append((char) signature[5]);
      streamMetadata.version = version.toString();

      streamMetadata.logicalScreenWidth = stream.readUnsignedShort();
      streamMetadata.logicalScreenHeight = stream.readUnsignedShort();

      int packedFields = stream.readUnsignedByte();
      boolean globalColorTableFlag = (packedFields & 0x80) != 0;
      streamMetadata.colorResolution = ((packedFields >> 4) & 0x7) + 1;
      streamMetadata.sortFlag = (packedFields & 0x8) != 0;
      int numGCTEntries = 1 << ((packedFields & 0x7) + 1);

      streamMetadata.backgroundColorIndex = stream.readUnsignedByte();
      streamMetadata.pixelAspectRatio = stream.readUnsignedByte();

      if (globalColorTableFlag) {
        streamMetadata.globalColorTable = new byte[3 * numGCTEntries];
        stream.readFully(streamMetadata.globalColorTable);
      } else {
        streamMetadata.globalColorTable = null;
      }

      // Found position of metadata for image 0
      imageStartPosition.add(Long.valueOf(stream.getStreamPosition()));
    } catch (IOException e) {
      throw new IIOException("I/O error reading header!", e);
    }

    gotHeader = true;
  }

  private boolean skipImage() throws IIOException {
    // Stream must be at the beginning of an image descriptor
    // upon exit

    try {
      while (true) {
        int blockType = stream.readUnsignedByte();

        if (blockType == 0x2c) {
          stream.skipBytes(8);

          int packedFields = stream.readUnsignedByte();
          if ((packedFields & 0x80) != 0) {
            // Skip color table if any
            int bits = (packedFields & 0x7) + 1;
            stream.skipBytes(3 * (1 << bits));
          }

          stream.skipBytes(1);

          int length = 0;
          do {
            length = stream.readUnsignedByte();
            stream.skipBytes(length);
          } while (length > 0);

          return true;
        } else if (blockType == 0x3b) {
          return false;
        } else if (blockType == 0x21) {
          int label = stream.readUnsignedByte();

          int length = 0;
          do {
            length = stream.readUnsignedByte();
            stream.skipBytes(length);
          } while (length > 0);
        } else if (blockType == 0x0) {
          // EOF
          return false;
        } else {
          int length = 0;
          do {
            length = stream.readUnsignedByte();
            stream.skipBytes(length);
          } while (length > 0);
        }
      }
    } catch (EOFException e) {
      return false;
    } catch (IOException e) {
      throw new IIOException("I/O error locating image!", e);
    }
  }

  private int locateImage(int imageIndex) throws IIOException {
    readHeader();

    try {
      // Find closest known index
      int index = Math.min(imageIndex, imageStartPosition.size() - 1);

      // Seek to that position
      Long l = (Long) imageStartPosition.get(index);
      stream.seek(l.longValue());

      // Skip images until at desired index or last image found
      while (index < imageIndex) {
        if (!skipImage()) {
          --index;
          return index;
        }

        Long l1 = new Long(stream.getStreamPosition());
        imageStartPosition.add(l1);
        ++index;
      }
    } catch (IOException e) {
      throw new IIOException("Couldn't seek!", e);
    }

    if (currIndex != imageIndex) {
      imageMetadata = null;
    }
    currIndex = imageIndex;
    return imageIndex;
  }

  // Read blocks of 1-255 bytes, stop at a 0-length block
  private byte[] concatenateBlocks() throws IOException {
    byte[] data = new byte[0];
    while (true) {
      int length = stream.readUnsignedByte();
      if (length == 0) {
        break;
      }
      byte[] newData = new byte[data.length + length];
      System.arraycopy(data, 0, newData, 0, data.length);
      stream.readFully(newData, data.length, length);
      data = newData;
    }

    return data;
  }

  // Stream must be positioned at start of metadata for 'currIndex'
  private void readMetadata() throws IIOException {
    if (stream == null) {
      throw new IllegalStateException("Input not set!");
    }

    try {
      // Create an object to store the image metadata
      this.imageMetadata = new GIFImageMetadata();

      long startPosition = stream.getStreamPosition();
      while (true) {
        int blockType = stream.readUnsignedByte();
        if (blockType == 0x2c) { // Image Descriptor
          imageMetadata.imageLeftPosition = stream.readUnsignedShort();
          imageMetadata.imageTopPosition = stream.readUnsignedShort();
          imageMetadata.imageWidth = stream.readUnsignedShort();
          imageMetadata.imageHeight = stream.readUnsignedShort();

          int idPackedFields = stream.readUnsignedByte();
          boolean localColorTableFlag = (idPackedFields & 0x80) != 0;
          imageMetadata.interlaceFlag = (idPackedFields & 0x40) != 0;
          imageMetadata.sortFlag = (idPackedFields & 0x20) != 0;
          int numLCTEntries = 1 << ((idPackedFields & 0x7) + 1);

          if (localColorTableFlag) {
            // Read color table if any
            imageMetadata.localColorTable = new byte[3 * numLCTEntries];
            stream.readFully(imageMetadata.localColorTable);
          } else {
            imageMetadata.localColorTable = null;
          }

          // Record length of this metadata block
          this.imageMetadataLength = (int) (stream.getStreamPosition() - startPosition);

          // Now positioned at start of LZW-compressed pixels
          return;
        } else if (blockType == 0x21) { // Extension block
          int label = stream.readUnsignedByte();

          if (label == 0xf9) { // Graphics Control Extension
            int gceLength = stream.readUnsignedByte(); // 4
            int gcePackedFields = stream.readUnsignedByte();
            imageMetadata.disposalMethod = (gcePackedFields >> 2) & 0x3;
            imageMetadata.userInputFlag = (gcePackedFields & 0x2) != 0;
            imageMetadata.transparentColorFlag = (gcePackedFields & 0x1) != 0;

            imageMetadata.delayTime = stream.readUnsignedShort();
            imageMetadata.transparentColorIndex = stream.readUnsignedByte();

            int terminator = stream.readUnsignedByte();
          } else if (label == 0x1) { // Plain text extension
            int length = stream.readUnsignedByte();
            imageMetadata.hasPlainTextExtension = true;
            imageMetadata.textGridLeft = stream.readUnsignedShort();
            imageMetadata.textGridTop = stream.readUnsignedShort();
            imageMetadata.textGridWidth = stream.readUnsignedShort();
            imageMetadata.textGridHeight = stream.readUnsignedShort();
            imageMetadata.characterCellWidth = stream.readUnsignedByte();
            imageMetadata.characterCellHeight = stream.readUnsignedByte();
            imageMetadata.textForegroundColor = stream.readUnsignedByte();
            imageMetadata.textBackgroundColor = stream.readUnsignedByte();
            imageMetadata.text = concatenateBlocks();
          } else if (label == 0xfe) { // Comment extension
            byte[] comment = concatenateBlocks();
            if (imageMetadata.comments == null) {
              imageMetadata.comments = new ArrayList();
            }
            imageMetadata.comments.add(comment);
          } else if (label == 0xff) { // Application extension
            int blockSize = stream.readUnsignedByte();
            byte[] applicationID = new byte[8];
            byte[] authCode = new byte[3];

            // read available data
            byte[] blockData = new byte[blockSize];
            stream.readFully(blockData);

            int offset = copyData(blockData, 0, applicationID);
            offset = copyData(blockData, offset, authCode);

            byte[] applicationData = concatenateBlocks();

            if (offset < blockSize) {
              int len = blockSize - offset;
              byte[] data = new byte[len + applicationData.length];

              System.arraycopy(blockData, offset, data, 0, len);
              System.arraycopy(applicationData, 0, data, len, applicationData.length);

              applicationData = data;
            }

            // Init lists if necessary
            if (imageMetadata.applicationIDs == null) {
              imageMetadata.applicationIDs = new ArrayList();
              imageMetadata.authenticationCodes = new ArrayList();
              imageMetadata.applicationData = new ArrayList();
            }
            imageMetadata.applicationIDs.add(applicationID);
            imageMetadata.authenticationCodes.add(authCode);
            imageMetadata.applicationData.add(applicationData);
          } else {
            // Skip over unknown extension blocks
            int length = 0;
            do {
              length = stream.readUnsignedByte();
              stream.skipBytes(length);
            } while (length > 0);
          }
        } else if (blockType == 0x3b) { // Trailer
          throw new IndexOutOfBoundsException("Attempt to read past end of image sequence!");
        } else {
          throw new IIOException("Unexpected block type " + blockType + "!");
        }
      }
    } catch (IIOException iioe) {
      throw iioe;
    } catch (IOException ioe) {
      throw new IIOException("I/O error reading image metadata!", ioe);
    }
  }

  private int copyData(byte[] src, int offset, byte[] dst) {
    int len = dst.length;
    int rest = src.length - offset;
    if (len > rest) {
      len = rest;
    }
    System.arraycopy(src, offset, dst, 0, len);
    return offset + len;
  }

  private void startPass(int pass) {
    if (updateListeners == null) {
      return;
    }

    int y = 0;
    int yStep = 1;
    if (imageMetadata.interlaceFlag) {
      y = interlaceOffset[interlacePass];
      yStep = interlaceIncrement[interlacePass];
    }

    int[] vals = ReaderUtil.computeUpdatedPixels(sourceRegion,
                                                 destinationOffset,
                                                 destinationRegion.x,
                                                 destinationRegion.y,
                                                 destinationRegion.x + destinationRegion.width - 1,
                                                 destinationRegion.y + destinationRegion.height - 1,
                                                 sourceXSubsampling,
                                                 sourceYSubsampling,
                                                 0,
                                                 y,
                                                 destinationRegion.width,
                                                 (destinationRegion.height + yStep - 1) / yStep,
                                                 1,
                                                 yStep);

    // Initialized updateMinY and updateYStep
    this.updateMinY = vals[1];
    this.updateYStep = vals[5];

    // Inform IIOReadUpdateListeners of new pass
    int[] bands = { 0 };

    processPassStarted(theImage,
                       interlacePass,
                       sourceMinProgressivePass,
                       sourceMaxProgressivePass,
                       0,
                       updateMinY,
                       1,
                       updateYStep,
                       bands);
  }

  public BufferedImage read(int imageIndex, ImageReadParam param) throws IIOException {
    if (stream == null) {
      throw new IllegalStateException("Input not set!");
    }
    checkIndex(imageIndex);

    int index = locateImage(imageIndex);
    if (index != imageIndex) {
      throw new IndexOutOfBoundsException("imageIndex out of bounds!");
    }

    clearAbortRequest();
    readMetadata();

    // A null ImageReadParam means we use the default
    if (param == null) {
      param = getDefaultReadParam();
    }

    // Initialize the destination image
    Iterator imageTypes = getImageTypes(imageIndex);
    this.theImage = getDestination(param,
                                   imageTypes,
                                   imageMetadata.imageWidth,
                                   imageMetadata.imageHeight);
    this.theTile = theImage.getWritableTile(0, 0);
    this.width = imageMetadata.imageWidth;
    this.height = imageMetadata.imageHeight;
    this.streamX = 0;
    this.streamY = 0;
    this.rowsDone = 0;
    this.interlacePass = 0;

    // Get source region, taking subsampling offsets into account,
    // and clipping against the true source bounds

    this.sourceRegion = new Rectangle(0, 0, 0, 0);
    this.destinationRegion = new Rectangle(0, 0, 0, 0);
    computeRegions(param, width, height, theImage, sourceRegion, destinationRegion);
    this.destinationOffset = new Point(destinationRegion.x, destinationRegion.y);

    this.sourceXSubsampling = param.getSourceXSubsampling();
    this.sourceYSubsampling = param.getSourceYSubsampling();
    this.sourceMinProgressivePass = Math.max(param.getSourceMinProgressivePass(), 0);
    this.sourceMaxProgressivePass = Math.min(param.getSourceMaxProgressivePass(), 3);

    this.destY = destinationRegion.y + (streamY - sourceRegion.y) / sourceYSubsampling;
    computeDecodeThisRow();

    // Inform IIOReadProgressListeners of start of image
    processImageStarted(imageIndex);
    startPass(0);

    this.rowBuf = new byte[width];

    try {
      // Read and decode the image data, fill in theImage
      this.initCodeSize = stream.readUnsignedByte();

      // Read first data block
      this.blockLength = stream.readUnsignedByte();
      int left = blockLength;
      int off = 0;
      while (left > 0) {
        int nbytes = stream.read(block, off, left);
        left -= nbytes;
        off += nbytes;
      }

      this.bitPos = 0;
      this.nextByte = 0;
      this.lastBlockFound = false;
      this.interlacePass = 0;

      // Init 32-bit buffer
      initNext32Bits();

      this.clearCode = 1 << initCodeSize;
      this.eofCode = clearCode + 1;

      int code, oldCode = 0;

      int[] prefix = new int[4096];
      byte[] suffix = new byte[4096];
      byte[] initial = new byte[4096];
      int[] length = new int[4096];
      byte[] string = new byte[4096];

      initializeStringTable(prefix, suffix, initial, length);
      int tableIndex = (1 << initCodeSize) + 2;
      int codeSize = initCodeSize + 1;
      int codeMask = (1 << codeSize) - 1;

      while (!abortRequested()) {
        code = getCode(codeSize, codeMask);

        if (code == clearCode) {
          initializeStringTable(prefix, suffix, initial, length);
          tableIndex = (1 << initCodeSize) + 2;
          codeSize = initCodeSize + 1;
          codeMask = (1 << codeSize) - 1;

          code = getCode(codeSize, codeMask);
          if (code == eofCode) {
            // Inform IIOReadProgressListeners of end of image
            processImageComplete();
            return theImage;
          }
        } else if (code == eofCode) {
          // Inform IIOReadProgressListeners of end of image
          processImageComplete();
          return theImage;
        } else {
          int newSuffixIndex;
          if (code < tableIndex) {
            newSuffixIndex = code;
          } else { // code == tableIndex
            newSuffixIndex = oldCode;
            if (code != tableIndex) {
              // warning - code out of sequence
              // possibly data corruption
              processWarningOccurred("Out-of-sequence code!");
            }
          }

          int ti = tableIndex;
          int oc = oldCode;

          prefix[ti] = oc;
          suffix[ti] = initial[newSuffixIndex];
          initial[ti] = initial[oc];
          length[ti] = length[oc] + 1;

          ++tableIndex;
          if ((tableIndex == (1 << codeSize)) && (tableIndex < 4096)) {
            ++codeSize;
            codeMask = (1 << codeSize) - 1;
          }
        }

        // Reverse code
        int c = code;
        int len = length[c];
        for (int i = len - 1; i >= 0; i--) {
          string[i] = suffix[c];
          c = prefix[c];
        }

        outputPixels(string, len);
        oldCode = code;
      }

      processReadAborted();
      return theImage;
    } catch (IOException e) {
      throw new IIOException("I/O error reading image!", e);
    }
  }

  /**
   * Remove all settings including global settings such as <code>Locale</code>s
   * and listeners, as well as stream settings.
   */
  public void reset() {
    super.reset();
    resetStreamSettings();
  }

  /**
   * Remove local settings based on parsing of a stream.
   */
  private void resetStreamSettings() {
    gotHeader = false;
    streamMetadata = null;
    currIndex = -1;
    imageMetadata = null;
    imageStartPosition = new ArrayList();
    numImages = -1;

    // No need to reinitialize 'block'
    blockLength = 0;
    bitPos = 0;
    nextByte = 0;

    next32Bits = 0;
    lastBlockFound = false;

    theImage = null;
    theTile = null;
    width = -1;
    height = -1;
    streamX = -1;
    streamY = -1;
    rowsDone = 0;
    interlacePass = 0;
  }
}
