/*-
 * Copyright (C) 2014 Erik Larsson
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.catacombae.storage.fs.hfsplus;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import org.catacombae.hfs.types.decmpfs.DecmpfsHeader;
import org.catacombae.hfsexplorer.IOUtil;
import org.catacombae.hfsexplorer.fs.ResourceForkReader;
import org.catacombae.hfsexplorer.types.resff.ReferenceListEntry;
import org.catacombae.hfsexplorer.types.resff.ResourceMap;
import org.catacombae.hfsexplorer.types.resff.ResourceType;
import org.catacombae.io.BasicReadableRandomAccessStream;
import org.catacombae.io.RandomAccessStream;
import org.catacombae.io.ReadableByteArrayStream;
import org.catacombae.io.ReadableRandomAccessInputStream;
import org.catacombae.io.ReadableRandomAccessStream;
import org.catacombae.io.RuntimeIOException;
import org.catacombae.io.SynchronizedReadableRandomAccessStream;
import org.catacombae.io.TruncatableRandomAccessStream;
import org.catacombae.io.WritableRandomAccessStream;
import org.catacombae.storage.fs.FSFork;
import org.catacombae.storage.fs.FSForkType;
import org.catacombae.util.Util;

/**
 * @author <a href="https://catacombae.org" target="_top">Erik Larsson</a>
 */
public class HFSPlusCompressedDataFork implements FSFork {
    private static final boolean DEBUG = Util.booleanEnabledByProperties(false,
            "org.catacombae.debug",
            "org.catacombae.storage.debug",
            "org.catacombae.storage.fs.debug",
            "org.catacombae.storage.fs.hfsplus.debug",
            "org.catacombae.storage.fs.hfsplus." +
            HFSPlusCompressedDataFork.class.getSimpleName() + ".debug");

    private final FSFork decmpfsFork;
    private final FSFork resourceFork;

    private DecmpfsHeader decmpfsHeader = null;

    private boolean lengthValid = false;
    private long length = 0;

    private boolean occupiedSizeValid = false;
    private long occupiedSize = 0;

    HFSPlusCompressedDataFork(FSFork decmpfsFork, FSFork resourceFork) {
        this.decmpfsFork = decmpfsFork;
        this.resourceFork = resourceFork;
    }

    public FSForkType getType() {
        return FSForkType.DATA;
    }

    synchronized DecmpfsHeader getDecmpfsHeader() {
        if(decmpfsHeader == null) {
            ReadableRandomAccessStream stream =
                    decmpfsFork.getReadableRandomAccessStream();
            try {
                byte[] headerData = new byte[DecmpfsHeader.STRUCTSIZE];

                stream.readFully(headerData);

                DecmpfsHeader header = new DecmpfsHeader(headerData, 0);
                if(header.getMagic() != DecmpfsHeader.MAGIC) {
                    throw new RuntimeException("Invalid magic for decmpfs " +
                            "header: \"" +
                            Util.toASCIIString(header.getRawMagic()) + "\"");
                }

                decmpfsHeader = header;
            } finally {
                if(stream != null) {
                    stream.close();
                }
            }
        }

        return decmpfsHeader;
    }

    public synchronized long getLength() {
        if(!lengthValid) {
            final DecmpfsHeader header = getDecmpfsHeader();

            length = header.getRawFileSize();
            lengthValid = true;
        }

        return length;
    }

    public synchronized long getOccupiedSize() {
        if(!occupiedSizeValid) {
            final DecmpfsHeader header = getDecmpfsHeader();
            final long tmpOccupiedSize;

            switch(header.getRawCompressionType()) {
                case DecmpfsHeader.COMPRESSION_TYPE_INLINE:
                    tmpOccupiedSize =
                            decmpfsFork.getLength() - DecmpfsHeader.STRUCTSIZE;
                    break;
                case DecmpfsHeader.COMPRESSION_TYPE_RESOURCE:
                    ReadableRandomAccessStream resourceForkStream = null;
                    ResourceForkReader r = null;
                    try {
                        resourceForkStream =
                            resourceFork.getReadableRandomAccessStream();
                        r = new ResourceForkReader(resourceForkStream);

                        final ResourceMap m = r.getResourceMap();
                        Long resourceDataLength = null;

                        for(ResourceType t : m.getResourceTypeList()) {
                            if(!Util.toASCIIString(t.getType()).equals("cmpf"))
                            {
                                continue;
                            }

                            final ReferenceListEntry[] entries =
                                    m.getReferencesByType(t);
                            if(entries.length != 1) {
                                throw new RuntimeException("More than one " +
                                        "instance (" + entries.length + ") " +
                                        "of resource type 'cmpf'.");
                            }

                            resourceDataLength = r.getDataLength(entries[0]);
                        }

                        if(resourceDataLength == null) {
                            throw new RuntimeException("No 'cmpf' resource " +
                                    "found in resource fork.");
                        }

                        tmpOccupiedSize = resourceDataLength;
                    } finally {
                        if(r != null) {
                            r.close();
                        }
                        else if(resourceForkStream != null) {
                            resourceForkStream.close();
                        }
                    }
                    break;
                default:
                    throw new RuntimeException("Unsupported compression type " +
                            header.getCompressionType());
            }

            occupiedSize = tmpOccupiedSize;
            occupiedSizeValid = true;
        }

        return occupiedSize;
    }

    public boolean isWritable() {
        return false;
    }

    public boolean isTruncatable() {
        return false;
    }

    public boolean isCompressed() {
        return true;
    }

    public String getForkIdentifier() {
        return "Data fork";
    }

    public boolean hasXattrName() {
        return false;
    }

    public String getXattrName() {
        return null;
    }

    public InputStream getInputStream() {
        return new ReadableRandomAccessInputStream(
                new SynchronizedReadableRandomAccessStream(
                        getReadableRandomAccessStream()));
    }

    public synchronized ReadableRandomAccessStream
            getReadableRandomAccessStream()
    {
        ReadableRandomAccessStream decmpfsForkStream = null;
        ReadableRandomAccessStream resourceForkStream = null;
        try {
            DecmpfsHeader header = getDecmpfsHeader();

            decmpfsForkStream = decmpfsFork.getReadableRandomAccessStream();

            final ReadableRandomAccessStream dataForkStream;
            final long compressionType = header.getCompressionType();

            if(compressionType == DecmpfsHeader.COMPRESSION_TYPE_INLINE) {
                /* Compressed file data is stored within the decmpfs fork
                 * itself. */
                final long fileSize = header.getRawFileSize();
                if(fileSize < 0 || fileSize > Integer.MAX_VALUE) {
                    System.err.println("Decompressed data is too large " +
                            "to be stored in memory.");
                    return null;
                }

                decmpfsForkStream.seek(DecmpfsHeader.STRUCTSIZE);
                byte compressionFlags = decmpfsForkStream.readFully();

                if((compressionFlags & 0x0F) == 0x0F) {
                    /* Data is stored as an uncompressed blob in the attributes
                     * file. */
                    final int uncompressedDataOffset =
                            DecmpfsHeader.STRUCTSIZE + 1;
                    final long uncompressedDataLength =
                            decmpfsForkStream.length() - uncompressedDataOffset;
                    if(uncompressedDataLength < 0 ||
                            uncompressedDataLength > Integer.MAX_VALUE)
                    {
                        System.err.println("Uncompressed data is too large " +
                                "to be stored in memory.");
                        return null;
                    }

                    if(uncompressedDataLength != fileSize) {
                        System.err.println("[WARNING] decmpfs compression " +
                                "type 3 uncompressed data length " +
                                "(" + uncompressedDataLength + " doesn't " +
                                "match file size (" + fileSize + ").");
                    }

                    final byte[] uncompressedData =
                            IOUtil.readFully(decmpfsForkStream,
                                    uncompressedDataOffset,
                                    (int) fileSize);

                    dataForkStream =
                            new ReadableByteArrayStream(uncompressedData);
                }
                else {
                    /* Data is stored as a compressed blob in the attributes
                     * file. */

                    /* Decompress data in memory and return a stream for reading
                     * from the resulting memory buffer. */
                    final int compressedDataOffset = DecmpfsHeader.STRUCTSIZE;
                    final long compressedDataLength =
                            decmpfsForkStream.length() - compressedDataOffset;
                    if(compressedDataLength < 0 ||
                            compressedDataLength > Integer.MAX_VALUE)
                    {
                        System.err.println("Compressed data is too large to " +
                                "be stored in memory.");
                        return null;
                    }

                    final byte[] compressedData =
                            IOUtil.readFully(decmpfsForkStream,
                                    compressedDataOffset,
                                    (int) compressedDataLength);

                    final Inflater inflater = new Inflater(false);
                    inflater.setInput(compressedData);

                    byte[] outBuffer = new byte[(int) fileSize];
                    try {
                        inflater.inflate(outBuffer);
                    } catch(DataFormatException ex) {
                        System.err.println("Invalid compressed data in " +
                                "decmpfs attribute. Exception stack trace:");
                        ex.printStackTrace();
                        return null;
                    }

                    final boolean inflaterFinished = inflater.finished();
                    inflater.end();

                    if(!inflaterFinished) {
                        System.err.println("Decompression failed. All input " +
                                "was not processed.");
                        return null;
                    }

                    dataForkStream = new ReadableByteArrayStream(outBuffer);
                }
            }
            else if(compressionType == DecmpfsHeader.COMPRESSION_TYPE_RESOURCE)
            {
                /* Compressed file data is stored in the resource fork. */
                resourceForkStream =
                        resourceFork.getReadableRandomAccessStream();

                final ResourceForkReader resReader =
                        new ResourceForkReader(resourceForkStream);
                final ResourceMap map = resReader.getResourceMap();

                ResourceType cmpfType = null;
                for(ResourceType curType : map.getResourceTypeList()) {
                    if(Util.toASCIIString(curType.getType()).equals("cmpf")) {
                        if(curType.getInstanceCount() > 0) {
                            System.err.println("Resource fork har more than " +
                                    "1 instance of \"cmpf\" resource (" +
                                    (curType.getInstanceCount() + 1) + " " +
                                    "instances). Don't know how to handle " +
                                    "this...");
                            return null;
                        }

                        cmpfType = curType;
                        break;
                    }
                }

                if(cmpfType == null) {
                    System.err.println("No \"cmpf\" resource found in " +
                            "resource fork.");
                    return null;
                }

                ReferenceListEntry[] referenceListEntries =
                        map.getReferencesByType(cmpfType);

                if(referenceListEntries.length != 1) {
                    System.err.println("Unexpected length of returned " +
                            "reference list entry array (expected: 1, " +
                            "actual: " + referenceListEntries.length);
                    return null;
                }

                dataForkStream = new CompressedResourceStream(
                        resReader.getResourceStream(referenceListEntries[0]),
                        header.getRawFileSize());
            }
            else {
                System.err.println("Unknown decmpfs compression type: " +
                        compressionType);

                return null;
            }

            return dataForkStream;
        } finally {
            if(resourceForkStream != null) {
                resourceForkStream.close();
            }

            if(decmpfsForkStream != null) {
                decmpfsForkStream.close();
            }
        }
    }

    public WritableRandomAccessStream getWritableRandomAccessStream()
            throws UnsupportedOperationException
    {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public RandomAccessStream getRandomAccessStream()
            throws UnsupportedOperationException
    {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public OutputStream getOutputStream() throws UnsupportedOperationException {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public TruncatableRandomAccessStream getForkStream()
            throws UnsupportedOperationException
    {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    boolean isUsingResourceFork() {
        return getDecmpfsHeader().getCompressionType() ==
                DecmpfsHeader.COMPRESSION_TYPE_RESOURCE;
    }

    private static class CompressedResourceStream
            extends BasicReadableRandomAccessStream
    {
        /* The compressed stream is divided into blocks, where each block is a
         * separate compression unit and can be individually decompressed.
         * The structure of the stream is as follows:
         *     le32 blockCount;
         *     struct {
         *         le32 blockOffset;
         *         le32 blockLength;
         *     } blockTable[blockCount];
         *     u8[...] compressedData;
         *
         * It is unclear if the whole resource stream must be rewritten when
         * data is updated (if not, then there's something about the compressed
         * format that we do not yet understand).
         * It is also unclear if the blocks always have the same uncompressed
         * size, i.e. if we can rely on this for seeking and in this case if the
         * block size is always equal to the file system block size. For now we
         * decompress every block when seeking, which is slow. */

        private final ReadableRandomAccessStream resourceStream;
        private final long uncompressedSize;
        private final int blockCount;
        private final byte[] blockTableData;
        private final Inflater inflater = new Inflater(true);

        private long fp = 0;

        private int processedBlocks = 0;
        private int fixedBlockSize = 0;
        private long[] nextBlockOffsets = null;

        public CompressedResourceStream(
                final ReadableRandomAccessStream resourceStream,
                final long uncompressedSize)
        {
            this.resourceStream = resourceStream;
            this.uncompressedSize = uncompressedSize;

            byte[] blockCountData = new byte[4];
            this.resourceStream.seek(0);
            this.resourceStream.readFully(blockCountData);
            this.blockCount = Util.readIntLE(blockCountData);

            if(DEBUG) {
                System.err.println("[CompressedResourceStream.<init>] " +
                        "blockCount=" + blockCount);
            }

            this.blockTableData = new byte[this.blockCount * (2 * 4)];
            this.resourceStream.readFully(this.blockTableData);
            if(DEBUG) {
                System.err.println("[CompressedResourceStream.<init>] Block " +
                        "table data:");
                for(int i = 0; i < blockCount; ++i) {
                    System.err.println("[CompressedResourceStream.<init>] " +
                            "    " + i + ": offset=" +
                            Util.readIntLE(blockTableData, 2*4*i) + ", " +
                            "length=" +
                            Util.readIntLE(blockTableData, 2*4*i + 4));
                }
            }
        }

        @Override
        public synchronized void close() throws RuntimeIOException {
            resourceStream.close();
        }

        @Override
        public synchronized void seek(long pos) throws RuntimeIOException {
            if(pos < 0) {
                throw new RuntimeIOException("Negative seek offset: " + pos);
            }

            fp = pos;
        }

        @Override
        public long length() throws RuntimeIOException {
            return uncompressedSize;
        }

        @Override
        public synchronized long getFilePointer() throws RuntimeIOException {
            return fp;
        }

        @Override
        public synchronized int read(byte[] data, int pos, int len)
                throws RuntimeIOException
        {
            if(DEBUG) {
                System.err.println("[CompressedResourceStream.read(byte[], " +
                        "int, int)] Called with data=" + data + ", pos=" + pos +
                        ", len=" + len + "...");
            }

            /* Input check. */
            if(data == null) {
                throw new IllegalArgumentException("data == null");
            }
            else if(pos < 0) {
                throw new IllegalArgumentException("pos < 0");
            }
            else if(len < 0) {
                throw new IllegalArgumentException("len < 0");
            }

            /* Read is completely beyond end of file => -1 (EOF). */
            if(fp >= uncompressedSize) {
                return -1;
            }

            /* Read is partially beyond end of file => truncate len. */
            if(len > uncompressedSize || fp > (uncompressedSize - len)) {
                len = (int) (uncompressedSize - fp);
            }

            int curBlock;
            long curFp;
            long curBlockOffset;

            if(processedBlocks == 0 || fixedBlockSize == 0) {
                /* Either no blocks have been processed previously, or the
                 * uncompressed block size is not fixed. In both cases we must
                 * start from the beginning (though if we have processed some
                 * blocks before fp previously we will still be able to use
                 * cached data to speed up the seek). */
                curBlock = 0;
                curFp = 0;
                curBlockOffset = 0;
            }
            else {
                /* We have previously processed some blocks, and the
                 * uncompressed block size has been identical so far, so we can
                 * calculate the uncompressed offset with one simple
                 * operation. */
                final long requestedBlock = fp / fixedBlockSize;

                if(requestedBlock > processedBlocks) {
                    curBlock = processedBlocks + 1;
                    curFp = curBlock * fixedBlockSize;
                    curBlockOffset = curFp;
                }
                else {
                    curBlock = (int) requestedBlock;
                    curFp = fp;
                    curBlockOffset = curBlock * fixedBlockSize;
                }
            }

            long endFp = fp + len;
            if(endFp > uncompressedSize) {
                endFp = uncompressedSize;
            }

            byte[] compressedBuffer = null;
            final byte[] decompressedBuffer = new byte[16 * 1024];
            int bytesRead = 0;

            while(curFp < endFp) {
                final boolean skip;

                if(DEBUG) {
                    System.err.println("[CompressedResourceStream." +
                            "read(byte[], int, int)]     Iterating... curFp " +
                            "(" + curFp + ") < endFp (" + endFp + ")");
                }

                if(curBlock < processedBlocks) {
                    final long nextBlockOffset;

                    /* We have cached data from a previous access. */
                    if(fixedBlockSize != 0) {
                        /* Fixed block size => we can easily calculate the
                         * offset of the next block. */
                        nextBlockOffset = curBlockOffset + fixedBlockSize;
                    }
                    else if(nextBlockOffsets == null) {
                        throw new RuntimeException("Unexpected: " +
                                "fixedBlockSize == 0 but no blockOffsets " +
                                "array!");
                    }
                    else {
                        /* Variable block size => we must look up the offset of
                         * the next block in nextBlockOffsets. */
                        nextBlockOffset = nextBlockOffsets[curBlock];
                    }

                    /* Now that we have the offset of the next block, we can
                     * determine whether this block can be skipped i.e. whether
                     * it is fully located before 'fp'. */
                    skip = nextBlockOffset <= fp;
                }
                else {
                    /* We do not know anything about this block, so we cannot
                     * skip past it. */
                    skip = false;
                }

                if(!skip) {
                    /* We cannot skip over this block since it might be within
                     * the range of data that we are requesting. So read the
                     * compressed data from disk and decompress it. */
                    final int curOffset =
                            Util.readIntLE(blockTableData, curBlock * (2 * 4));
                    final int curLength =
                            Util.readIntLE(blockTableData, curBlock * (2 * 4) +
                                    4);

                    int curDecompressedOffsetInBlock = 0;

                    resourceStream.seek(curOffset);
                    final byte compressionFlags = resourceStream.readFully();

                    if((compressionFlags & 0x0F) == 0x0F) {
                        /* Block is not compressed... just copy from input to
                         * output. */
                        final int rawDataOffset = curOffset + 1;
                        final int rawDataLength = curLength - 1;

                        if(DEBUG) {
                            System.err.println("[CompressedResourceStream." +
                                    "read(byte[], int, int)]     Copying " +
                                    "raw data at logical offset " +
                                    curFp + ": [offset=" + rawDataOffset +
                                    ", length=" + rawDataLength + "]");
                        }

                        final int remainingLen = len - bytesRead;
                        final int remainingInBlock = rawDataLength;
                        final int curBytesToRead =
                                remainingInBlock < remainingLen ?
                                remainingInBlock : remainingLen;

                        /* If the raw data is requested by the caller, i.e. if
                         * the file pointer is within the current block as we
                         * know it so far (file pointer is greater than the
                         * start of the block and less than the current
                         * decompressed end offset, then copy this data to the
                         * destination array. */
                        if(fp >= curBlockOffset &&
                                fp < (curBlockOffset + curLength))
                        {
                            resourceStream.readFully(data, pos + bytesRead,
                                    curBytesToRead);
                        }

                        curDecompressedOffsetInBlock = curLength;
                    }
                    else {
                        /* Block is compressed. Decompression is necessary. */

                        /* Read compressed block into memory. We assume that it
                         * will not be too large to fit in memory. If this
                         * assumption breaks we can restructure the code to read
                         * the compressed data in chunks, but let's not do that
                         * now since it will only complicate things. */

                        final int compressedDataOffset = curOffset + 2;
                        final int compressedDataLength = curLength - 2;

                        if(DEBUG) {
                            System.err.println("[CompressedResourceStream." +
                                    "read(byte[], int, int)]     " +
                                    "Decompressing compressed data at " +
                                    "logical offset " + curFp + ": [offset=" +
                                    compressedDataOffset + ", length=" +
                                    compressedDataLength + "]");
                        }

                        if(compressedBuffer == null ||
                                compressedBuffer.length < compressedDataLength)
                        {
                            if(compressedBuffer != null) {
                                /* Explicitly let GC reclaim old buffer when we
                                 * are allocating the new one to avoid running
                                 * out of memory due to unnecessary references
                                 * to old allocations (the VM is probably smart
                                 * enough so that this isn't necessary, but just
                                 * in case). */
                                compressedBuffer = null;
                            }

                            compressedBuffer = new byte[compressedDataLength];
                        }

                        resourceStream.seek(compressedDataOffset);
                        resourceStream.readFully(compressedBuffer, 0,
                                compressedDataLength);

                        /* Decompress data in current block. */

                        inflater.reset();
                        inflater.setInput(compressedBuffer, 0,
                                compressedDataLength);

                        while(!inflater.finished()) {
                            final int inflatedBytes;
                            try {
                                inflatedBytes =
                                        inflater.inflate(decompressedBuffer);
                            } catch(DataFormatException ex) {
                                throw new RuntimeException("Invalid " +
                                        "compressed data in resource fork " +
                                        "(" + ex + ").", ex);
                            }

                            if(DEBUG) {
                                System.err.println("Inflated " + inflatedBytes +
                                        " to decompressedBuffer (length: " +
                                        decompressedBuffer.length + ").");
                            }

                            if(inflatedBytes <= 0) {
                                throw new RuntimeIOException("No " +
                                        "(" + inflatedBytes + ") inflated " +
                                        "bytes. inflater.needsInput()=" +
                                        inflater.needsInput() + " " +
                                        "inflater.needsDictionary()=" +
                                        inflater.needsDictionary());
                            }

                            /* If the decompressed data is requested by the
                             * caller, i.e. if the file pointer is within the
                             * current block as we know it so far (file pointer
                             * is greater than the start of the block and less
                             * than the current decompressed end offset, then
                             * copy this data to the destination array. */
                            final long curOffsetInBlock =
                                    fp - curBlockOffset;
                            if(curOffsetInBlock >=
                                    curDecompressedOffsetInBlock &&
                                    curOffsetInBlock <
                                    (curDecompressedOffsetInBlock +
                                    inflatedBytes))
                            {
                                final int inOffset =
                                        (int) (curOffsetInBlock -
                                        curDecompressedOffsetInBlock);
                                final int remainingBytes = len - bytesRead;
                                final int copyLength =
                                        remainingBytes < inflatedBytes ?
                                        remainingBytes : inflatedBytes;

                                if(DEBUG) {
                                    System.err.println("Copying " + copyLength +
                                            " bytes from decompressedBuffer " +
                                            "@ " + inOffset + " to data @ " +
                                            (pos + bytesRead) + " " +
                                            "(curDecompressedOffsetInBlock=" +
                                            curDecompressedOffsetInBlock +
                                            ", fp=" + fp + ", curBlockOffset=" +
                                            curBlockOffset + ")...");
                                }

                                System.arraycopy(decompressedBuffer, inOffset,
                                        data, pos + bytesRead, copyLength);

                                fp += copyLength;
                                bytesRead += copyLength;
                            }
                            else {
                                if(DEBUG) {
                                    System.err.println("Skipping copy of " +
                                            "data outside bounds of read. " +
                                            "fp=" + fp + " curBlockOffset=" +
                                            curBlockOffset + " " +
                                            "curOffsetInBlock=" +
                                            curOffsetInBlock + " " +
                                            "curDecompressedOffsetInBlock=" +
                                            curDecompressedOffsetInBlock + " " +
                                            "inflatedBytes=" + inflatedBytes);
                                }
                            }

                            curDecompressedOffsetInBlock += inflatedBytes;
                            curFp += inflatedBytes;
                        }

                        if(DEBUG) {
                            System.err.println("Inflater is finished.");
                        }
                    }

                    if(curBlock == processedBlocks) {
                        /* Current block has been decompressed. Update info
                         * about this block since we haven't visited it
                         * before. */
                        if(processedBlocks == 0) {
                            fixedBlockSize = curDecompressedOffsetInBlock;
                        }
                        else if(fixedBlockSize == 0 ||
                                curDecompressedOffsetInBlock != fixedBlockSize)
                        {
                            if(nextBlockOffsets == null ||
                                    nextBlockOffsets.length <
                                    (processedBlocks + 1))
                            {
                                long[] oldNextBlockOffsets =
                                        nextBlockOffsets;

                                nextBlockOffsets =
                                        new long[processedBlocks + 1];
                                if(oldNextBlockOffsets != null) {
                                    System.arraycopy(oldNextBlockOffsets, 0,
                                            nextBlockOffsets, 0,
                                            oldNextBlockOffsets.length);
                                }
                                else {
                                    for(int i = 0; i < processedBlocks; ++i) {
                                        nextBlockOffsets[i] =
                                                (i + 1) * fixedBlockSize;
                                    }
                                }
                            }

                            nextBlockOffsets[processedBlocks] =
                                    nextBlockOffsets[processedBlocks - 1] +
                                    curDecompressedOffsetInBlock;
                            fixedBlockSize = 0;
                        }

                        ++processedBlocks;
                    }
                    else if(curBlock > processedBlocks) {
                        throw new RuntimeException("Internal error: Went " +
                                "beyond processed blocks.");
                    }
                }

                curBlockOffset += fixedBlockSize != 0 ? fixedBlockSize :
                        nextBlockOffsets[curBlock] -
                        (curBlock == 0 ? 0 : nextBlockOffsets[curBlock - 1]);
                ++curBlock;
            }

            if(DEBUG) {
                System.err.println("[CompressedResourceStream.read(byte[], " +
                        "int, int)] Leaving with " +
                        (bytesRead == 0 ? -1 : bytesRead) + ".");
            }

            return bytesRead == 0 ? -1 : bytesRead;
        }
    }
}
