/*
 * Copyright (c) 2014, Harald Kuhr
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * 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.
 *     * Neither the name "TwelveMonkeys" nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * 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 OWNER 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.
 */

package com.twelvemonkeys.imageio.plugins.bmp;

import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.io.LittleEndianDataInputStream;
import com.twelvemonkeys.io.enc.DecoderStream;
import com.twelvemonkeys.xml.XMLSerializer;

import javax.imageio.*;
import javax.imageio.event.IIOReadUpdateListener;
import javax.imageio.event.IIOReadWarningListener;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.io.DataInput;
import java.io.File;
import java.io.IOException;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Iterator;

/**
 * ImageReader for Microsoft Windows Bitmap (BMP) format.
 *
 * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
 * @author last modified by $Author: haraldk$
 * @version $Id: CURImageReader.java,v 1.0 Apr 20, 2009 11:54:28 AM haraldk Exp$
 * @see com.twelvemonkeys.imageio.plugins.bmp.ICOImageReader
 */
public final class BMPImageReader extends ImageReaderBase {
    private long pixelOffset;
    private DIBHeader header;
    private int[] colors;
    private IndexColorModel colorMap;

    private ImageReader jpegReaderDelegate;
    private ImageReader pngReaderDelegate;

    public BMPImageReader() {
        super(new BMPImageReaderSpi());
    }

    protected BMPImageReader(final ImageReaderSpi pProvider) {
        super(pProvider);
    }

    @Override
    protected void resetMembers() {
        pixelOffset = 0;
        header = null;
        colors = null;
        colorMap = null;

        if (pngReaderDelegate != null) {
            pngReaderDelegate.dispose();
            pngReaderDelegate = null;
        }
        if (jpegReaderDelegate != null) {
            jpegReaderDelegate.dispose();
            jpegReaderDelegate = null;
        }
    }

    @Override
    public int getNumImages(boolean allowSearch) throws IOException {
        readHeader();

        return 1;
    }

    private void readHeader() throws IOException {
        assertInput();

        if (header == null) {
            // BMP files have Intel origin, always little endian
            imageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN);

            // Read BMP file header
            byte[] fileHeader = new byte[DIB.BMP_FILE_HEADER_SIZE - 4]; // We'll read the last 4 bytes later
            imageInput.readFully(fileHeader);

            if (fileHeader[0] != 'B' || fileHeader[1] != 'M') {
                throw new IIOException("Not a BMP");
            }

            // Ignore rest of data, it's redundant...
            pixelOffset = imageInput.readUnsignedInt();

            // Read DIB header
            header = DIBHeader.read(imageInput);
        }
    }

    private IndexColorModel readColorMap() throws IOException {
        readHeader();

        if (colors == null) {
            if (header.getBitCount() > 8 && header.colorsUsed == 0) {
                // RGB without color map
                colors = new int[0];
            }
            else {
                int offset = DIB.BMP_FILE_HEADER_SIZE + header.getSize();
                if (offset != imageInput.getStreamPosition()) {
                    imageInput.seek(offset);
                }

                if (header.getSize() == DIB.BITMAP_CORE_HEADER_SIZE) {
                    colors = new int[Math.min(header.getColorsUsed(), (int) (pixelOffset - DIB.BMP_FILE_HEADER_SIZE - header.getSize()) / 3)];

                    // Byte triplets in BGR form
                    for (int i = 0; i < colors.length; i++) {
                        int b = imageInput.readUnsignedByte();
                        int g = imageInput.readUnsignedByte();
                        int r = imageInput.readUnsignedByte();
                        colors[i] = r << 16 | g << 8 | b | 0xff000000;
                    }
                }
                else {
                    colors = new int[Math.min(header.getColorsUsed(), (int) (pixelOffset - DIB.BMP_FILE_HEADER_SIZE - header.getSize()) / 4)];

                    // Byte quadruples in BGRa (or little-endian ints in aRGB) form, where a is "Reserved"
                    for (int i = 0; i < colors.length; i++) {
                        colors[i] = imageInput.readInt() & 0x00ffffff | 0xff000000;
                    }
                }

                if (colors.length > 0) {
                    // There might be more entries in the color map, but we ignore these for reading
                    int mapSize = Math.min(colors.length, 1 << header.getBitCount());

                    // Compute bits for > 8 bits (used only for meta data)
                    int bits = header.getBitCount() <= 8 ? header.getBitCount() : mapSize <= 256 ? 8 : 16;

                    colorMap = new IndexColorModel(bits, mapSize, colors, 0, false, -1, DataBuffer.TYPE_BYTE);
                }
            }
        }

        return colorMap;
    }

    @Override
    public int getWidth(int pImageIndex) throws IOException {
        checkBounds(pImageIndex);

        return header.getWidth();
    }

    @Override
    public int getHeight(int pImageIndex) throws IOException {
        checkBounds(pImageIndex);

        return header.getHeight();
    }

    @Override
    public Iterator<ImageTypeSpecifier> getImageTypes(int pImageIndex) throws IOException {
        checkBounds(pImageIndex);

        // TODO: Better implementation, include INT_RGB types for 3BYTE_BGR and 4BYTE_ABGR for INT_ARGB
        return Arrays.asList(getRawImageType(pImageIndex)).iterator();
    }

    @Override
    public ImageTypeSpecifier getRawImageType(int pImageIndex) throws IOException {
        checkBounds(pImageIndex);

        if (header.getPlanes() != 1) {
            throw new IIOException("Multiple planes not supported");
        }

        switch (header.getBitCount()) {
            case 1:
            case 2:
            case 4:
            case 8:
                return ImageTypeSpecifiers.createFromIndexColorModel(readColorMap());

            case 16:
                if (header.hasMasks()) {
                    int[] masks = getMasks();

                    return ImageTypeSpecifiers.createPacked(
                            ColorSpace.getInstance(ColorSpace.CS_sRGB),
                            masks[0], masks[1], masks[2], masks[3],
                            DataBuffer.TYPE_USHORT, false
                    );
                }

                // Default if no mask is 555
                return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_USHORT_555_RGB);

            case 24:
                if (header.getCompression() != DIB.COMPRESSION_RGB) {
                    throw new IIOException("Unsupported compression for RGB: " + header.getCompression());
                }

                return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR);

            case 32:
                if (header.hasMasks()) {
                    int[] masks = getMasks();

                    return ImageTypeSpecifiers.createPacked(
                            ColorSpace.getInstance(ColorSpace.CS_sRGB),
                            masks[0], masks[1], masks[2], masks[3],
                            DataBuffer.TYPE_INT, false
                    );
                }

                // Default if no mask
                return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB);

            case 0:
                if (header.getCompression() == DIB.COMPRESSION_JPEG || header.getCompression() == DIB.COMPRESSION_PNG) {
                    return initReaderDelegate(header.getCompression()).getRawImageType(0);
                }
            default:
                throw new IIOException("Unsupported bit count: " + header.getBitCount());
        }
    }

    private int[] getMasks() throws IOException {
        if (header.masks != null) {
            // Get mask and create either 555, 565 or 444/4444 etc
            return header.masks;
        }

        switch (header.getCompression()) {
            case DIB.COMPRESSION_BITFIELDS:
            case DIB.COMPRESSION_ALPHA_BITFIELDS:
                // Consult BITFIELDS/ALPHA_BITFIELDS
                return readBitFieldsMasks();
            default:
                return null;
        }
    }

    private int[] readBitFieldsMasks() throws IOException {
        long offset = DIB.BMP_FILE_HEADER_SIZE + header.getSize();

        if (offset != imageInput.getStreamPosition()) {
            imageInput.seek(offset);
        }

        int[] masks = DIBHeader.readMasks(imageInput);

        if (header.getCompression() != DIB.COMPRESSION_ALPHA_BITFIELDS) {
            masks[3] = 0;
        }

        return masks;
    }

    @Override
    public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException {
        checkBounds(imageIndex);

        // Delegate reading for JPEG/PNG compression
        if (header.getCompression() == DIB.COMPRESSION_JPEG || header.getCompression() == DIB.COMPRESSION_PNG) {
            return readUsingDelegate(header.getCompression(), param);
        }

        int width = getWidth(imageIndex);
        int height = getHeight(imageIndex);

        ImageTypeSpecifier rawType = getRawImageType(imageIndex);
        BufferedImage destination = getDestination(param, getImageTypes(imageIndex), width, height);

        ColorModel colorModel = destination.getColorModel();
        if (colorModel instanceof IndexColorModel && ((IndexColorModel) colorModel).getMapSize() < header.getColorsUsed()) {
            processWarningOccurred(
                    String.format("Color map contains more colors than raster allows (%d). Ignoring entries above %d.",
                            header.getColorsUsed(), ((IndexColorModel) colorModel).getMapSize())
            );
        }

        // BMP rows are padded to 4 byte boundary
        int rowSizeBytes = ((header.getBitCount() * width + 31) / 32) * 4;

        // Wrap input according to compression
        imageInput.seek(pixelOffset);
        DataInput input;

        switch (header.getCompression()) {
            case DIB.COMPRESSION_RLE4:
                if (header.getBitCount() != 4) {
                    throw new IIOException(String.format("Unsupported combination of bitCount/compression: %s/%s", header.getBitCount(), header.getCompression()));
                }

                input = new LittleEndianDataInputStream(new DecoderStream(IIOUtil.createStreamAdapter(imageInput), new RLE4Decoder(width), rowSizeBytes));
                break;

            case DIB.COMPRESSION_RLE8:
                if (header.getBitCount() != 8) {
                    throw new IIOException(String.format("Unsupported combination of bitCount/compression: %s/%s", header.getBitCount(), header.getCompression()));
                }
                input = new LittleEndianDataInputStream(new DecoderStream(IIOUtil.createStreamAdapter(imageInput), new RLE8Decoder(width), rowSizeBytes));
                break;

            case DIB.COMPRESSION_BITFIELDS:
            case DIB.COMPRESSION_ALPHA_BITFIELDS:
                // TODO: Validate bitCount for these
            case DIB.COMPRESSION_RGB:
                input = imageInput;
                break;

            default:
                throw new IIOException("Unsupported compression: " + header.getCompression());

        }

        Rectangle srcRegion = new Rectangle();
        Rectangle destRegion = new Rectangle();
        computeRegions(param, width, height, destination, srcRegion, destRegion);

        WritableRaster destRaster = clipToRect(destination.getRaster(), destRegion, param != null ? param.getDestinationBands() : null);
        checkReadParamBandSettings(param, rawType.getNumBands(), destRaster.getNumBands());

        WritableRaster rowRaster;

        switch (header.getBitCount()) {
            case 1:
            case 2:
            case 4:
                rowRaster = Raster.createPackedRaster(new DataBufferByte(rowSizeBytes), width, 1, header.getBitCount(), null);
                break;
            case 8:
            case 24:
                rowRaster = Raster.createInterleavedRaster(new DataBufferByte(rowSizeBytes), width, 1, rowSizeBytes, header.getBitCount() / 8, createOffsets(rawType.getNumBands()), null);
                break;
            case 16:
            case 32:
                rowRaster = rawType.createBufferedImage(width, 1).getRaster();
                break;
            default:
                throw new IIOException("Unsupported pixel depth: " + header.getBitCount());
        }

        // Clip to source region
        Raster clippedRow = clipRowToRect(rowRaster, srcRegion,
                param != null ? param.getSourceBands() : null,
                param != null ? param.getSourceXSubsampling() : 1);

        int xSub = param != null ? param.getSourceXSubsampling() : 1;
        int ySub = param != null ? param.getSourceYSubsampling() : 1;

        processImageStarted(imageIndex);
        for (int y = 0; y < height; y++) {
            switch (header.getBitCount()) {
                case 1:
                case 2:
                case 4:
                case 8:
                case 24:
                    byte[] rowDataByte = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
                    readRowByte(input, height, srcRegion, xSub, ySub, rowDataByte, destRaster, clippedRow, y);
                    break;

                case 16:
                    short[] rowDataUShort = ((DataBufferUShort) rowRaster.getDataBuffer()).getData();
                    readRowUShort(input, height, srcRegion, xSub, ySub, rowDataUShort, destRaster, clippedRow, y);
                    break;

                case 32:
                    int[] rowDataInt = ((DataBufferInt) rowRaster.getDataBuffer()).getData();
                    readRowInt(input, height, srcRegion, xSub, ySub, rowDataInt, destRaster, clippedRow, y);
                    break;

                default:
                    throw new AssertionError("Unsupported pixel depth: " + header.getBitCount());
            }

            processImageProgress(100f * y / height);

            if (height - 1 - y < srcRegion.y) {
                break;
            }

            if (abortRequested()) {
                processReadAborted();
                break;
            }
        }

        processImageComplete();

        return destination;
    }

    private BufferedImage readUsingDelegate(final int compression, final ImageReadParam param) throws IOException {
        return initReaderDelegate(compression).read(0, param);
    }

    private ImageReader initReaderDelegate(int compression) throws IOException {
        ImageReader reader = getImageReaderDelegate(compression);

        imageInput.seek(pixelOffset);
        reader.setInput(new SubImageInputStream(imageInput, header.getImageSize()));

        return reader;
    }

    private ImageReader getImageReaderDelegate(int compression) throws IIOException {
        String format;

        switch (compression) {
            case DIB.COMPRESSION_JPEG:
                if (jpegReaderDelegate != null) {
                    return jpegReaderDelegate;
                }

                format = "JPEG";
                break;

            case DIB.COMPRESSION_PNG:
                if (pngReaderDelegate != null) {
                    return pngReaderDelegate;
                }

                format = "PNG";
                break;

            default:
                throw new AssertionError("Unsupported BMP compression: " + compression);
        }

        // Consider looking for specific PNG and JPEG implementations.
        Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName(format);

        if (!readers.hasNext()) {
            throw new IIOException(String.format("Delegate ImageReader for %s format not found", format));
        }

        ImageReader reader = readers.next();

        // Install listener
        ListenerDelegator listenerDelegator = new ListenerDelegator();
        reader.addIIOReadWarningListener(listenerDelegator);
        reader.addIIOReadProgressListener(listenerDelegator);
        reader.addIIOReadUpdateListener(listenerDelegator);

        // Cache for later use
        switch (compression) {
            case DIB.COMPRESSION_JPEG:
                jpegReaderDelegate = reader;
                break;
            case DIB.COMPRESSION_PNG:
                pngReaderDelegate = reader;
                break;
        }

        return reader;
    }

    private int[] createOffsets(int numBands) {
        int[] offsets = new int[numBands];

        for (int i = 0; i < numBands; i++) {
            offsets[i] = numBands - i - 1;
        }

        return offsets;
    }

    private void readRowByte(final DataInput input, final int height, final Rectangle srcRegion, final int xSub, final int ySub,
                             final byte[] rowDataByte, final WritableRaster destChannel, final Raster srcChannel, final int y) throws IOException {
        // If subsampled or outside source region, skip entire row
        if (y % ySub != 0 || height - 1 - y < srcRegion.y || height - 1 - y >= srcRegion.y + srcRegion.height) {
            input.skipBytes(rowDataByte.length);

            return;
        }

        input.readFully(rowDataByte, 0, rowDataByte.length);

        // Subsample horizontal
        if (xSub != 1) {
            for (int x = 0; x < srcRegion.width / xSub; x++) {
                rowDataByte[srcRegion.x + x] = rowDataByte[srcRegion.x + x * xSub];
            }
        }

        if (header.topDown) {
            destChannel.setDataElements(0, y, srcChannel);
        } else {
            // Flip into position
            int dstY = (height - 1 - y - srcRegion.y) / ySub;
            destChannel.setDataElements(0, dstY, srcChannel);
        }
    }

    private void readRowUShort(final DataInput input, final int height, final Rectangle srcRegion, final int xSub, final int ySub,
                               final short[] rowDataUShort, final WritableRaster destChannel, final Raster srcChannel, final int y) throws IOException {
        // If subsampled or outside source region, skip entire row
        if (y % ySub != 0 || height - 1 - y < srcRegion.y || height - 1 - y >= srcRegion.y + srcRegion.height) {
            input.skipBytes(rowDataUShort.length * 2 + (rowDataUShort.length % 2) * 2);

            return;
        }

        readFully(input, rowDataUShort);

        // Skip 2 bytes, if not ending on 32 bit/4 byte boundary
        if (rowDataUShort.length % 2 != 0) {
            input.skipBytes(2);
        }

        // Subsample horizontal
        if (xSub != 1) {
            for (int x = 0; x < srcRegion.width / xSub; x++) {
                rowDataUShort[srcRegion.x + x] = rowDataUShort[srcRegion.x + x * xSub];
            }
        }

        if (header.topDown) {
            destChannel.setDataElements(0, y, srcChannel);
        } else {
            // Flip into position
            int dstY = (height - 1 - y - srcRegion.y) / ySub;
            destChannel.setDataElements(0, dstY, srcChannel);
        }
    }

    private void readRowInt(final DataInput input, final int height, final Rectangle srcRegion, final int xSub, final int ySub,
                            final int[] rowDataInt, final WritableRaster destChannel, final Raster srcChannel, final int y) throws IOException {
        // If subsampled or outside source region, skip entire row
        if (y % ySub != 0 || height - 1 - y < srcRegion.y || height - 1 - y >= srcRegion.y + srcRegion.height) {
            input.skipBytes(rowDataInt.length * 4);

            return;
        }

        readFully(input, rowDataInt);

        // Subsample horizontal
        if (xSub != 1) {
            for (int x = 0; x < srcRegion.width / xSub; x++) {
                rowDataInt[srcRegion.x + x] = rowDataInt[srcRegion.x + x * xSub];
            }
        }

        if (header.topDown) {
            destChannel.setDataElements(0, y, srcChannel);
        } else {
            // Flip into position
            int dstY = (height - 1 - y - srcRegion.y) / ySub;
            destChannel.setDataElements(0, dstY, srcChannel);
        }
    }

    // TODO: Candidate util method
    private static void readFully(final DataInput input, final short[] shorts) throws IOException {
        if (input instanceof ImageInputStream) {
            // Optimization for ImageInputStreams, read all in one go
            ((ImageInputStream) input).readFully(shorts, 0, shorts.length);
        } else {
            for (int i = 0; i < shorts.length; i++) {
                shorts[i] = input.readShort();
            }
        }
    }

    // TODO: Candidate util method
    private static void readFully(final DataInput input, final int[] ints) throws IOException {
        if (input instanceof ImageInputStream) {
            // Optimization for ImageInputStreams, read all in one go
            ((ImageInputStream) input).readFully(ints, 0, ints.length);
        } else {
            for (int i = 0; i < ints.length; i++) {
                ints[i] = input.readInt();
            }
        }
    }

    private Raster clipRowToRect(final Raster raster, final Rectangle rect, final int[] bands, final int xSub) {
        if (rect.contains(raster.getMinX(), 0, raster.getWidth(), 1)
                && xSub == 1
                && bands == null /* TODO: Compare bands with that of raster */) {
            return raster;
        }

        return raster.createChild(rect.x / xSub, 0, rect.width / xSub, 1, 0, 0, bands);
    }

    private WritableRaster clipToRect(final WritableRaster raster, final Rectangle rect, final int[] bands) {
        if (rect.contains(raster.getMinX(), raster.getMinY(), raster.getWidth(), raster.getHeight())
                && bands == null /* TODO: Compare bands with that of raster */) {
            return raster;
        }

        return raster.createWritableChild(rect.x, rect.y, rect.width, rect.height, 0, 0, bands);
    }

    @Override
    public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
        readHeader();

        switch (header.getBitCount()) {
            case 1:
            case 2:
            case 4:
            case 8:
                readColorMap();
                break;

            default:
                if (header.colorsUsed > 0) {
                    readColorMap();
                }
                break;
        }

        // Why, oh why..? Instead of accepting it's own native format as it should,
        // The BMPImageWriter only accepts instances of com.sun.imageio.plugins.bmp.BMPMetadata...
        // TODO: Consider reflectively construct a BMPMetadata and inject fields
        return new BMPMetadata(header, colors);
    }

    public static void main(String[] args) throws IOException {
        BMPImageReaderSpi provider = new BMPImageReaderSpi();
        BMPImageReader reader = new BMPImageReader(provider);

        for (String arg : args) {
            try {
                File in = new File(arg);
                ImageInputStream stream = ImageIO.createImageInputStream(in);

                System.err.println("Can read?: " + provider.canDecodeInput(stream));

                reader.reset();
                reader.setInput(stream);

                ImageReadParam param = reader.getDefaultReadParam();
                param.setDestinationType(reader.getImageTypes(0).next());
                //            param.setSourceSubsampling(2, 3, 0, 0);
                //            param.setSourceSubsampling(2, 1, 0, 0);
                //
                //            int width = reader.getWidth(0);
                //            int height = reader.getHeight(0);
                //
                //            param.setSourceRegion(new Rectangle(width / 4, height / 4, width / 2, height / 2));
                //            param.setSourceRegion(new Rectangle(width / 2, height / 2));
                //            param.setSourceRegion(new Rectangle(width / 2, height / 2, width / 2, height / 2));

                System.err.println("reader.header: " + reader.header);

                BufferedImage image = reader.read(0, param);

                System.err.println("image: " + image);

                showIt(image, in.getName());


                IIOMetadata imageMetadata = reader.getImageMetadata(0);
                if (imageMetadata != null) {
                    new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(imageMetadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false);
                }
            }
            catch (Throwable t) {
                if (args.length > 1) {
                    System.err.println("---");
                    System.err.println("---> " + t.getClass().getSimpleName() + ": " + t.getMessage() + " for " + arg);
                    System.err.println("---");
                }
                else {
                    throwAs(RuntimeException.class, t);
                }
            }
        }
    }

    @SuppressWarnings({"unchecked", "UnusedDeclaration"})
    static <T extends Throwable> void throwAs(final Class<T> pType, final Throwable pThrowable) throws T {
        throw (T) pThrowable;
    }

    private class ListenerDelegator extends ProgressListenerBase implements IIOReadUpdateListener, IIOReadWarningListener {
        @Override
        public void imageComplete(ImageReader source) {
            processImageComplete();
        }

        @Override
        public void imageProgress(ImageReader source, float percentageDone) {
            processImageProgress(percentageDone);
        }

        @Override
        public void imageStarted(ImageReader source, int imageIndex) {
            processImageStarted(imageIndex);
        }

        @Override
        public void readAborted(ImageReader source) {
            processReadAborted();
        }

        @Override
        public void sequenceComplete(ImageReader source) {
            processSequenceComplete();
        }

        @Override
        public void sequenceStarted(ImageReader source, int minIndex) {
            processSequenceStarted(minIndex);
        }

        @Override
        public void thumbnailComplete(ImageReader source) {
            processThumbnailComplete();
        }

        @Override
        public void thumbnailProgress(ImageReader source, float percentageDone) {
            processThumbnailProgress(percentageDone);
        }

        @Override
        public void thumbnailStarted(ImageReader source, int imageIndex, int thumbnailIndex) {
            processThumbnailStarted(imageIndex, thumbnailIndex);
        }

        public void passStarted(ImageReader source, BufferedImage theImage, int pass, int minPass, int maxPass, int minX, int minY, int periodX, int periodY, int[] bands) {
            processPassStarted(theImage, pass, minPass, maxPass, minX, minY, periodX, periodY, bands);
        }

        public void imageUpdate(ImageReader source, BufferedImage theImage, int minX, int minY, int width, int height, int periodX, int periodY, int[] bands) {
            processImageUpdate(theImage, minX, minY, width, height, periodX, periodY, bands);
        }

        public void passComplete(ImageReader source, BufferedImage theImage) {
            processPassComplete(theImage);
        }

        public void thumbnailPassStarted(ImageReader source, BufferedImage theThumbnail, int pass, int minPass, int maxPass, int minX, int minY, int periodX, int periodY, int[] bands) {
            processThumbnailPassStarted(theThumbnail, pass, minPass, maxPass, minX, minY, periodX, periodY, bands);
        }

        public void thumbnailUpdate(ImageReader source, BufferedImage theThumbnail, int minX, int minY, int width, int height, int periodX, int periodY, int[] bands) {
            processThumbnailUpdate(theThumbnail, minX, minY, width, height, periodX, periodY, bands);
        }

        public void thumbnailPassComplete(ImageReader source, BufferedImage theThumbnail) {
            processThumbnailPassComplete(theThumbnail);
        }

        public void warningOccurred(ImageReader source, String warning) {
            processWarningOccurred(warning);
        }
    }
}
