/*
 * #%L
 * BSD implementations of Bio-Formats readers and writers
 * %%
 * Copyright (C) 2005 - 2017 Open Microscopy Environment:
 *   - Board of Regents of the University of Wisconsin-Madison
 *   - Glencoe Software, Inc.
 *   - University of Dundee
 * %%
 * 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 COPYRIGHT HOLDERS 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 COPYRIGHT HOLDERS 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.
 * #L%
 */

package loci.formats.in;

import java.io.EOFException;
import java.io.InputStream;
import java.io.IOException;
import java.util.Vector;
import java.util.zip.InflaterInputStream;

import loci.common.RandomAccessInputStream;
import loci.formats.CoreMetadata;
import loci.formats.FormatException;
import loci.formats.FormatReader;
import loci.formats.FormatTools;
import loci.formats.MetadataTools;
import loci.formats.UnsupportedCompressionException;
import loci.formats.meta.MetadataStore;

/**
 * APNGReader is the file format reader for
 * Animated Portable Network Graphics (APNG) images.
 *
 * @author Melissa Linkert melissa at glencoesoftware.com
 */
public class APNGReader extends FormatReader {

  // -- Constants --

  /** Color types. */
  private static final int GRAYSCALE = 0;
  private static final int TRUE_COLOR = 2;
  private static final int INDEXED = 3;
  private static final int GRAY_ALPHA = 4;
  private static final int TRUE_ALPHA = 6;

  /** Filter types. */
  private static final int NONE = 0;
  private static final int SUB = 1;
  private static final int UP = 2;
  private static final int AVERAGE = 3;
  private static final int PAETH = 4;

  /** Interlacing pass dimensions. */
  private static final int[] PASS_WIDTHS = {1, 1, 2, 2, 4, 4, 8};
  private static final int[] PASS_HEIGHTS = {1, 1, 1, 2, 2, 4, 4};

  // -- Fields --

  private Vector<PNGBlock> blocks;
  private Vector<int[]> frameCoordinates;

  private byte[][] lut;

  private byte[] lastImage;
  private int lastImageIndex = -1;
  private int lastImageRow = -1;

  private int compression;
  private int interlace;
  private int idatCount = 0;

  // -- Constructor --

  /** Constructs a new APNGReader. */
  public APNGReader() {
    super("Animated PNG", "png");
    domains = new String[] {FormatTools.GRAPHICS_DOMAIN};
    suffixNecessary = false;
  }

  // -- IFormatReader API methods --

  /* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */
  @Override
  public boolean isThisType(RandomAccessInputStream stream) throws IOException {
    final int blockLen = 8;
    if (!FormatTools.validStream(stream, blockLen, false)) return false;

    byte[] signature = new byte[blockLen];
    stream.read(signature);

    if (signature[0] != (byte) 0x89 || signature[1] != 0x50 ||
      signature[2] != 0x4e || signature[3] != 0x47 || signature[4] != 0x0d ||
      signature[5] != 0x0a || signature[6] != 0x1a || signature[7] != 0x0a)
    {
      return false;
    }
    return true;
  }

  /* @see loci.formats.IFormatReader#get8BitLookupTable() */
  @Override
  public byte[][] get8BitLookupTable() {
    FormatTools.assertId(currentId, true, 1);
    return lut;
  }

  /**
   * @see loci.formats.IFormatReader#openBytes(int, byte[], int, int, int, int)
   */
  @Override
  public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h)
    throws FormatException, IOException
  {
    FormatTools.checkPlaneParameters(this, no, buf.length, x, y, w, h);

    if (no == lastImageIndex && lastImage != null && y + h <= lastImageRow) {
      
      try (RandomAccessInputStream s = new RandomAccessInputStream(lastImage)) {
        readPlane(s, x, y, w, h, buf);
      }
      return buf;
    }

    if (no == 0) {
      lastImage = null;

      try (PNGInputStream stream = new PNGInputStream("IDAT")) {
        int decodeHeight = y + h;
        if (decodeHeight < getSizeY() && decodeHeight % 8 != 0) {
          decodeHeight += Math.min(8 - (decodeHeight % 8), getSizeY() - decodeHeight);
        }
        lastImage = decode(stream, getSizeX(), decodeHeight);
      }
      lastImageIndex = 0;
      lastImageRow = y + h;

      try (RandomAccessInputStream pix = new RandomAccessInputStream(lastImage)) {
        readPlane(pix, x, y, w, h, buf);
      }
      if (y + h < getSizeY()) {
        lastImage = null;
      }
      return buf;
    }


    int[] coords = frameCoordinates.get(no);

    lastImage = openBytes(0);
    lastImageRow = getSizeY();
    byte[] newImage = null;
    try (PNGInputStream stream = new PNGInputStream("fdAT", no)) {
      newImage = decode(stream, coords[2], coords[3]);
    }

    // paste current image onto first image

    int bpp = FormatTools.getBytesPerPixel(getPixelType());
    int len = coords[2] * bpp;
    int plane = getSizeX() * getSizeY() * bpp;
    int newPlane = len * coords[3];
    if (!isInterleaved()) {
      for (int c=0; c<getRGBChannelCount(); c++) {
        for (int row=0; row<coords[3]; row++) {
          System.arraycopy(newImage, c * newPlane + row * len, lastImage,
            c * plane + (coords[1] + row) * getSizeX() * bpp + coords[0] * bpp,
            len);
        }
      }
    }
    else {
      len *= getRGBChannelCount();
      for (int row=0; row<coords[3]; row++) {
        System.arraycopy(newImage, row * len, lastImage,
          (coords[1] + row) * getSizeX() * bpp * getRGBChannelCount() +
          coords[0] * bpp * getRGBChannelCount(), len);
      }
    }

    lastImageIndex = no;

    try (RandomAccessInputStream pix = new RandomAccessInputStream(lastImage)) {
      readPlane(pix, x, y, w, h, buf);
    }
    return buf;
  }

  /* @see loci.formats.IFormatReader#close(boolean) */
  @Override
  public void close(boolean fileOnly) throws IOException {
    super.close(fileOnly);
    if (!fileOnly) {
      lut = null;
      frameCoordinates = null;
      blocks = null;
      lastImage = null;
      lastImageIndex = -1;
      lastImageRow = -1;
      compression = 0;
      interlace = 0;
      idatCount = 0;
    }
  }

  // -- Internal FormatReader methods --

  /* @see loci.formats.FormatReader#initFile(String) */
  @Override
  protected void initFile(String id) throws FormatException, IOException {
    super.initFile(id);
    in = new RandomAccessInputStream(id);
    CoreMetadata m = core.get(0);

    // check that this is a valid PNG file
    byte[] signature = new byte[8];
    in.read(signature);

    if (signature[0] != (byte) 0x89 || signature[1] != 0x50 ||
      signature[2] != 0x4e || signature[3] != 0x47 || signature[4] != 0x0d ||
      signature[5] != 0x0a || signature[6] != 0x1a || signature[7] != 0x0a)
    {
      throw new FormatException("Invalid PNG signature.");
    }

    // read data chunks - each chunk consists of the following:
    // 1) 32 bit length
    // 2) 4 char type
    // 3) 'length' bytes of data
    // 4) 32 bit CRC

    blocks = new Vector<PNGBlock>();
    frameCoordinates = new Vector<int[]>();

    while (in.getFilePointer() < in.length()) {
      int length = in.readInt();
      String type = in.readString(4);

      PNGBlock block = new PNGBlock();
      block.length = length;
      block.type = type;
      block.offset = in.getFilePointer();
      blocks.add(block);

      if (type.equals("acTL")) {
        // APNG-specific chunk
        m.imageCount = in.readInt();
        int loop = in.readInt();
        addGlobalMeta("Loop count", loop);
      }
      else if (type.equals("fcTL")) {
        in.skipBytes(4);
        int w = in.readInt();
        int h = in.readInt();
        int x = in.readInt();
        int y = in.readInt();
        frameCoordinates.add(new int[] {x, y, w, h});
        in.skipBytes(length - 20);
      }
      else if (type.equals("IDAT")) {
        idatCount++;
      }
      else if (type.equals("PLTE")) {
        // lookup table

        m.indexed = true;
        lut = new byte[3][256];

        for (int i=0; i<length/3; i++) {
          for (int c=0; c<3; c++) {
            lut[c][i] = in.readByte();
          }
        }
      }
      else if (type.equals("IHDR")) {
        m.sizeX = in.readInt();
        m.sizeY = in.readInt();
        m.bitsPerPixel = in.read();
        int colorType = in.read();
        compression = in.read();
        int filter = in.read();
        interlace = in.read();

        if (filter != 0) {
          throw new FormatException("Invalid filter mode: " + filter);
        }

        switch (colorType) {
          case GRAYSCALE:
          case INDEXED:
            m.sizeC = 1;
            break;
          case GRAY_ALPHA:
            m.sizeC = 2;
            break;
          case TRUE_COLOR:
            m.sizeC = 3;
            break;
          case TRUE_ALPHA:
            m.sizeC = 4;
            break;
        }

        m.pixelType = getBitsPerPixel() <= 8 ?
          FormatTools.UINT8 : FormatTools.UINT16;
        m.rgb = getSizeC() > 1;
      }
      else if (type.equals("IEND")) {
        break;
      }
      in.seek(block.offset + length);

      if (in.getFilePointer() < in.length() - 4) {
        in.skipBytes(4); // skip the CRC
      }
    }

    if (m.imageCount == 0) m.imageCount = 1;
    m.sizeZ = 1;
    m.sizeT = getImageCount();

    m.dimensionOrder = "XYCTZ";
    m.interleaved = isRGB();
    m.falseColor = false;

    MetadataStore store = makeFilterMetadata();
    MetadataTools.populatePixels(store, this);
  }

  private byte[] decode(PNGInputStream bytes) throws FormatException, IOException {
    return decode(bytes, getSizeX(), getSizeY());
  }

  private byte[] decode(PNGInputStream bytes, int width, int height)
    throws FormatException, IOException
  {
    int bpp = FormatTools.getBytesPerPixel(getPixelType());
    int rowLen = width * getRGBChannelCount() * bpp;

    if (width > getSizeX()) {
      throw new FormatException(
        "Width (" + width + ") exceeds image width (" +  getSizeX() + ")");
    }
    if (height > getSizeY()) {
      throw new FormatException(
        "Height (" + height + ") exceeds image height (" +  getSizeY() + ")");
    }
    if (getBitsPerPixel() < bpp * 8) {
      int div = (bpp * 8) / getBitsPerPixel();
      if (div < rowLen) {
        int originalRowLen = rowLen;
        rowLen /= div;
        if (rowLen * div < originalRowLen) {
          rowLen++;
        }
      }
      else {
        rowLen = 1;
      }
    }

    byte[] image = null;

    if (compression == 0 && interlace == 0) {
      // decompress the image
      byte[] filters = new byte[height];
      image = new byte[rowLen * height];

      try (InflaterInputStream decompressor = new InflaterInputStream(bytes)) {
        int n = 0;
        for (int row=0; row<height; row++) {
          n = 0;
          while (n < 1) {
            n = decompressor.read(filters, row, 1);
          }
          n = 0;
          while (n < rowLen) {
            n += decompressor.read(image, row * rowLen + n, rowLen - n);
          }
        }
      }

      // perform any necessary unfiltering

      unfilter(filters, image, width, height);
    }
    else if (compression == 0) {
      // see: http://www.w3.org/TR/PNG/#8Interlace
      int byteCount = 0;

      byte[][] passImages = new byte[7][];

      int nRowBlocks = getSizeY() / 8;
      int nColBlocks = getSizeX() / 8;

      // row and column counts have to be adjusted when the image
      // dimensions are not multiples of 8

      if (8 * nRowBlocks != getSizeY()) {
        nRowBlocks++;
      }
      if (8 * nColBlocks != getSizeX()) {
        nColBlocks++;
      }

      if (nRowBlocks <= 0) {
        nRowBlocks = 1;
      }
      if (nColBlocks <= 0) {
        nColBlocks = 1;
      }

      image = new byte[FormatTools.getPlaneSize(this)];

      try (InflaterInputStream decompressor = new InflaterInputStream(bytes)) {
        for (int i=0; i<passImages.length; i++) {
          int passWidth = PASS_WIDTHS[i] * nColBlocks;
          int passHeight = PASS_HEIGHTS[i] * nRowBlocks;

          // see http://www.libpng.org/pub/png/spec/1.2/PNG-DataRep.html#DR.Interlaced-data-order
          // for clarification of the pass width / pass row adjustments

          if (nColBlocks * 8 != width) {
            int extraCols = getSizeX() - (nColBlocks - 1) * 8;
            switch (extraCols) {
              case 1:
                if (i == 1 || i == 3 || i == 5) {
                  passWidth -= PASS_WIDTHS[i];
                }
                if (i == 2 || i == 4 || i == 6) {
                  passWidth -= (PASS_WIDTHS[i] - 1);
                }
                break;
              case 2:
                if (i == 1 || i == 3) {
                  passWidth -= PASS_WIDTHS[i];
                }
                if (i == 2 || i == 4 || i == 5) {
                  passWidth -= (PASS_WIDTHS[i] - 1);
                }
                if (i == 6) {
                  passWidth -= (PASS_WIDTHS[i] - 2);
                }
                break;
              case 3:
                if (i == 1) {
                  passWidth -= PASS_WIDTHS[i];
                }
                if (i == 2 || i == 3 || i == 5) {
                  passWidth -= (PASS_WIDTHS[i] - 1);
                }
                if (i == 4) {
                  passWidth -= (PASS_WIDTHS[i] - 2);
                }
                if (i == 6) {
                  passWidth -= (PASS_WIDTHS[i] - 3);
                }
                break;
              case 4:
                if (i == 1) {
                  passWidth -= PASS_WIDTHS[i];
                }
                if (i == 2 || i == 3) {
                  passWidth -= (PASS_WIDTHS[i] - 1);
                }
                if (i == 4 || i == 5) {
                  passWidth -= (PASS_WIDTHS[i] - 2);
                }
                if (i == 6) {
                  passWidth -= (PASS_WIDTHS[i] - 4);
                }
                break;
              case 5:
                if (i == 3) {
                  passWidth -= (PASS_WIDTHS[i] - 1);
                }
                if (i == 5) {
                  passWidth -= (PASS_WIDTHS[i] - 2);
                }
                if (i == 4) {
                  passWidth -= (PASS_WIDTHS[i] - 3);
                }
                if (i == 6) {
                  passWidth -= (PASS_WIDTHS[i] - 5);
                }
                break;
              case 6:
                if (i == 3) {
                  passWidth -= (PASS_WIDTHS[i] - 1);
                }
                if (i == 4 || i == 5) {
                  passWidth -= (PASS_WIDTHS[i] - 3);
                }
                if (i == 6) {
                  passWidth -= (PASS_WIDTHS[i] - 6);
                }
                break;
              case 7:
                if (i == 5 || i == 6) {
                  passWidth--;
                }
                break;
            }
          }

          int rowSize = passWidth * bpp * getRGBChannelCount();

          byte[] filters = new byte[passHeight];
          passImages[i] = new byte[rowSize * passHeight];

          for (int row=0; row<passHeight; row++) {
            if (passWidth == 0) {
              continue;
            }
            if (nRowBlocks * 8 != getSizeY() && row >= PASS_HEIGHTS[i] * (nRowBlocks - 1)) {
              int extraRows = getSizeY() - (nRowBlocks - 1) * 8;
              switch (extraRows) {
                case 1:
                  if (i == 2 || i == 4 || i == 6) {
                    continue;
                  }
                  if ((i == 3 || i == 5) && (row % PASS_HEIGHTS[i]) > 0) {
                    continue;
                  }
                  break;
                case 2:
                  if (i == 4 || i == 2) {
                    continue;
                  }
                  if ((i == 3 || i == 5 || i == 6) && (row % PASS_HEIGHTS[i]) > 0) {
                    continue;
                  }
                  break;
                case 3:
                  if (i == 2) {
                    continue;
                  }
                  if ((i == 3 || i == 4 || i == 6) && (row % PASS_HEIGHTS[i]) > 0) {
                    continue;
                  }
                  if (i == 5 && (row % PASS_HEIGHTS[i] > 1)) {
                    continue;
                  }
                  break;
                case 4:
                  if (i == 2) {
                    continue;
                  }
                  if ((i == 3 || i == 4) && (row % PASS_HEIGHTS[i]) > 0) {
                    continue;
                  }
                  if ((i == 5 || i == 6) && (row % PASS_HEIGHTS[i]) > 1) {
                    continue;
                  }
                  break;
                case 5:
                  if ((i == 4) && (row % PASS_HEIGHTS[i]) > 0) {
                    continue;
                  }
                  if ((i == 6) && (row % PASS_HEIGHTS[i]) > 1) {
                    continue;
                  }
                  if ((i == 5) && (row % PASS_HEIGHTS[i]) > 2) {
                    continue;
                  }
                  break;
                case 6:
                  if ((i == 4) && (row % PASS_HEIGHTS[i]) > 0) {
                    continue;
                  }
                  if ((i == 5 || i == 6) && (row % PASS_HEIGHTS[i]) > 2) {
                    continue;
                  }
                  break;
                case 7:
                  if (i == 6 && (row % PASS_HEIGHTS[i]) > 2) {
                    continue;
                  }
                  break;
              }
            }
            int n = 0;
            while (n < 1) {
              n = decompressor.read(filters, row, 1);
            }
            n = 0;
            while (n < rowSize) {
              n += decompressor.read(passImages[i],
                row * rowSize + n, rowSize - n);
            }
          }

          unfilter(filters, passImages[i], passWidth, passHeight);
        }
      }

      int chunk = bpp * getRGBChannelCount();
      int[] passOffset = new int[7];

      for (int row=0; row<8 * (height / 8); row++) {
        int rowOffset = row * width * chunk;
        for (int col=0; col<width; col++) {
          int blockRow = row % 8;
          int blockCol = col % 8;

          int pass = -1;

          if ((blockRow % 2) == 1) {
            pass = 6;
          }
          else if (blockRow == 0 || blockRow == 4) {
            if ((blockCol % 2) == 1) {
              pass = 5;
            }
            else if (blockCol == 0) {
              pass = blockRow == 0 ? 0 : 2;
            }
            else if (blockCol == 4) {
              pass = blockRow == 0 ? 1 : 2;
            }
            else {
              pass = 3;
            }
          }
          else {
            pass = 4 + (blockCol % 2);
          }

          int colOffset = col * chunk;
          for (int c=0; c<chunk; c++) {
            if (passOffset[pass] < passImages[pass].length) {
              image[rowOffset + colOffset + c] =
                passImages[pass][passOffset[pass]++];
            }
          }
        }
      }
    }
    else {
      throw new UnsupportedCompressionException(
        "Compression type " + compression + " not supported");
    }

    // de-interleave

    if (getBitsPerPixel() < 8) {
      byte[] expandedImage = new byte[FormatTools.getPlaneSize(this)];
      try (RandomAccessInputStream bits = new RandomAccessInputStream(image)) {
        int skipBits = rowLen * 8 - getSizeX() * getBitsPerPixel();
        for (int row=0; row<getSizeY(); row++) {
          for (int col=0; col<getSizeX(); col++) {
            int index = row * getSizeX() + col;
            expandedImage[index] =
              (byte) (bits.readBits(getBitsPerPixel()) & 0xff);
          }
          bits.skipBits(skipBits);
        }
      }

      image = expandedImage;
    }

    return image;
  }

  /** See http://www.w3.org/TR/PNG/#9Filters. */
  private void unfilter(byte[] filters, byte[] image, int width, int height)
    throws FormatException
  {
    int bpp =
      getRGBChannelCount() * FormatTools.getBytesPerPixel(getPixelType());
    int rowLen = width * bpp;
    for (int row=0; row<height; row++) {
      int filter = filters[row];

      if (filter == NONE) {
        continue;
      }

      for (int col=0; col<rowLen; col++) {
        int q = row * rowLen + col;

        int xx = image[q] & 0xff;
        int a = col >= bpp ? image[q - bpp] & 0xff : 0;
        int b = row > 0 ? image[q - bpp * width] & 0xff : 0;
        int c = row > 0 && col >= bpp ?
          image[q - bpp * (width + 1)] & 0xff : 0;

        switch (filter) {
          case SUB:
            image[q] = (byte) ((xx + a) & 0xff);
            break;
          case UP:
            image[q] = (byte) ((xx + b) & 0xff);
            break;
          case AVERAGE:
            image[q] = (byte) ((xx + ((int) Math.floor(a + b) / 2)) & 0xff);
            break;
          case PAETH:
            int p = a + b - c;
            int pa = (int) Math.abs(p - a);
            int pb = (int) Math.abs(p - b);
            int pc = (int) Math.abs(p - c);

            if (pa <= pb && pa <= pc) {
              image[q] = (byte) ((xx + a) & 0xff);
            }
            else if (pb <= pc) {
              image[q] = (byte) ((xx + b) & 0xff);
            }
            else {
              image[q] = (byte) ((xx + c) & 0xff);
            }
            break;
          default:
            throw new FormatException("Unknown filter: " + filter);
        }
      }
    }
  }

  // -- Helper class --

  class PNGBlock {
    public long offset;
    public int length;
    public String type;
  }

  /**
   * InputStream implementation that stitches together IDAT blocks into
   * a seamless zlib stream.  This allows us to decompress the image data
   * without reading the entire compressed stream into a byte array.
   */
  class PNGInputStream extends InputStream {
    private int currentBlock = -1;
    private int blockPointer = 0;
    private int blockLength = 0;
    private String blockType;
    private int imageNumber;
    private int fctlCount = 0;
    private boolean fdatValid = false;

    public PNGInputStream(String blockType) throws IOException {
      this(blockType, 0);
    }

    public PNGInputStream(String blockType, int imageNumber) throws IOException {
      this.imageNumber = imageNumber;
      this.blockType = blockType;
      fctlCount = 0;
      fdatValid = false;
      advanceBlock();
    }

    @Override
    public int available() throws IOException {
      if (blockPointer == blockLength) {
        advanceBlock();
      }
      if (currentBlock < 0 || in.getFilePointer() == in.length()) {
        return -1;
      }
      return (int) Math.min(blockLength - blockPointer, in.length() - in.getFilePointer());
    }

    @Override
    public int read() throws IOException {
      if (blockPointer < blockLength) {
        blockPointer++;
        return in.read();
      }
      advanceBlock();
      if (currentBlock < 0) {
        throw new EOFException();
      }
      blockPointer++;
      return in.read();
    }

    public byte readByte() throws IOException {
      if (blockPointer < blockLength) {
        blockPointer++;
        return in.readByte();
      }
      advanceBlock();
      if (currentBlock < 0) {
        throw new EOFException();
      }
      blockPointer++;
      return in.readByte();
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
      int read = 0;
      for (int i=0; i<len; i++) {
        if (available() > 0) {
          b[off + i] = readByte();
          read++;
        }
        else {
          break;
        }
      }
      return read;
    }

    private void advanceBlock() throws IOException {
      if (currentBlock < blocks.size() - 1) {
        while (currentBlock < blocks.size()) {
          currentBlock++;
          if (currentBlock == blocks.size()) {
            currentBlock = -1;
            break;
          }
          if (blockType.equals("fdAT") && blocks.get(currentBlock).type.equals("fcTL")) {
            fdatValid = fctlCount == imageNumber;
            fctlCount++;
            if (fctlCount > imageNumber + 1) {
              currentBlock = -1;
              break;
            }
          }
          else if (blockType.equals(blocks.get(currentBlock).type)) {
            if (fdatValid || !blockType.equals("fdAT")) {
              break;
            }
          }
        }
        if (currentBlock >= 0) {
          blockPointer = 0;
          blockLength = blocks.get(currentBlock).length;
          in.seek(blocks.get(currentBlock).offset);
          if (blocks.get(currentBlock).type.equals("fdAT")) {
            blockLength -= 4;
            in.skipBytes(4);
          }
        }
      }
      else {
        currentBlock = -1;
        blockPointer = 0;
        blockLength = 0;
      }
    }

  }

}
