/*
 * @(#)PGMImageReader.java  1.1  2010-06-24
 * 
 * Copyright (c) 2009-2010 Werner Randelshofer, Immensee, Switzerland.
 * All rights reserved.
 *
 * You may not use, copy or modify this file, except in compliance with the
 * license agreement you entered into with Werner Randelshofer.
 * For details see accompanying license terms.
 */
package org.monte.media.pgm;

import java.awt.Point;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.image.BufferedImage;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferShort;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.stream.ImageInputStream;

/**
 * Reads an image in the Netpbm grayscale image format (PGM).
 * <p>
 * See: <a href="http://netpbm.sourceforge.net/doc/pgm.html">PGM Format Specification</a>.
 *
 * @author Werner Randelshofer
 * @version 1.1 2010-06-24 Skip comments in header.
 * <br>1.0 2009-12-14 Created.
 */
public class PGMImageReader extends ImageReader {

    /** All images have the same width.*/
    private int width = -1;
    /** All images have the same height.*/
    private int height = -1;
    /** All images have the same depth. Must be in the range [1,65535].*/
    private int maxGray = -1;
    /** Number of images. */
    private int numImages = -1;
    /** Start of image data. */
    private long dataOffset = -1;

    public PGMImageReader(PGMImageReaderSpi originatingProvider) {
        super(originatingProvider);
    }

    @Override
    public int getNumImages(boolean allowSearch) throws IOException {
        if (allowSearch && numImages == -1) {
            readHeader();

            ImageInputStream in = (ImageInputStream) getInput();
            in.seek(dataOffset);
            int dataSize = width * height * (maxGray > 255 ? 2 : 1);
            numImages = 0;
            while (in.skipBytes(dataSize) == dataSize) {
                numImages++;
            }
        }
        return numImages;
    }

    @Override
    public int getWidth(int imageIndex) throws IOException {
        readHeader();
        return width;
    }

    @Override
    public int getHeight(int imageIndex) throws IOException {
        readHeader();
        return height;
    }

    @Override
    public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException {
        readHeader();
        LinkedList<ImageTypeSpecifier> l = new LinkedList<ImageTypeSpecifier>();
        ComponentColorModel ccm = new ComponentColorModel(//
                new ICC_ColorSpace(ICC_Profile.getInstance(ColorSpace.CS_GRAY)),
                new int[]{maxGray > 255 ? 16 : 8},//
                false, false, Transparency.OPAQUE,//
                (maxGray > 255) ? DataBuffer.TYPE_SHORT : DataBuffer.TYPE_BYTE);
        l.add(new ImageTypeSpecifier(ccm, ccm.createCompatibleSampleModel(width, height)));
        return l.iterator();
    }

    @Override
    public IIOMetadata getStreamMetadata() throws IOException {
        return null;
    }

    @Override
    public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
        return null;
    }

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

        if (imageIndex > 0) {
            throw new ArrayIndexOutOfBoundsException("imageIndex is " + imageIndex + " must be 0.");
        }

        ImageInputStream in = (ImageInputStream) getInput();
        in.seek(dataOffset + imageIndex * width * height * (maxGray > 255 ? 2 : 1));

        ComponentColorModel ccm = new ComponentColorModel(//
                new ICC_ColorSpace(ICC_Profile.getInstance(ColorSpace.CS_GRAY)),
                new int[]{maxGray > 255 ? 16 : 8},//
                false, false, Transparency.OPAQUE,//
                (maxGray > 255) ? DataBuffer.TYPE_SHORT : DataBuffer.TYPE_BYTE);
        SampleModel sm = ccm.createCompatibleSampleModel(width, height);

        BufferedImage img;
        if (maxGray > 255) {
            DataBufferShort db = new DataBufferShort(width * height);
            in.readFully(db.getData(), 0, width * height);
            img = new BufferedImage(ccm, Raster.createWritableRaster(sm, db, new Point(0, 0)), false, new Hashtable());
        } else {
            DataBufferByte db = new DataBufferByte(width * height);
            in.readFully(db.getData(), 0, width * height);
            img = new BufferedImage(ccm, Raster.createWritableRaster(sm, db, new Point(0, 0)), false, new Hashtable());
        }

        return img;
    }

    /** Reads the PGM header.
     * Does nothing if the header has already been loaded.
     */
    private void readHeader() throws IOException {
        if (dataOffset == -1) {

            ImageInputStream in = (ImageInputStream) getInput();
            in.seek(0);

            // Check if file starts with "P5"
            if (in.readShort() != 0x5035) {
                in.reset();
                throw new IOException("Illegal magic number");
            }
            // Skip whitespace (blank, TAB, CR or LF)
            int b = in.readUnsignedByte();
            if (b != 0x20 && b != 0x09 && b != 0x0d && b != 0x0a) {
                throw new IOException("Whitespace missing after magic number");
            }
            // Read width
            width = readHeaderValue(in, "image width");
            if (width < 1) {
                throw new IOException("Illegal image width " + width);
            }
            height = readHeaderValue(in, "image height");
            if (height < 1) {
                throw new IOException("Illegal image width " + height);
            }
            maxGray = readHeaderValue(in, "maximum gray value");
            if (maxGray < 2 || maxGray > 65536) {
                throw new IOException("Illegal maximum gray value " + maxGray);
            }
            dataOffset = in.getStreamPosition();
        }
    }

    private int readHeaderValue(ImageInputStream in, String name) throws IOException {
        // Skip whitespace (blank, TAB, CR or LF) and comments
        int b;
        do {
            b = in.readUnsignedByte();

            if (b == '#') { // comments
                do {
                    b = in.readUnsignedByte();
                } while (b != 0x0d && b != 0x0a);
            }
        } while (b == 0x20 || b == 0x09 || b == 0x0d || b == 0x0a);

        // read value
        if (b < 0x30 || b > 0x39) {
            throw new IOException(name + " missing");
        }
        int value = 0;
        do {
            if (value >= 100000) {
                throw new IOException(name + " is too large");
            }
            value = value * 10 + b - 0x30;
        } while ((b = in.readUnsignedByte()) >= 0x30 && b <= 0x39);
        if (b != 0x20 && b != 0x09 && b != 0x0d && b != 0x0a) {
            throw new IOException("Whitespace after " + name + " missing");
        }
        return value;
    }
}
