/*
 * Copyright (C) 2009  Trustin Heuiseung Lee
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
// Derived from Michael Meiwald's PNM decoder:
// http://www.mms-computing.co.uk/uk/co/mmscomputing/imageio/ppm/
package net.gleamynode.imageio.pnm;

import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.util.Collections;
import java.util.Iterator;

import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;

class PortableAnymapImageReader extends ImageReader {

    static final int TYPE_PBM_ASCII = 0x5031;
    static final int TYPE_PGM_ASCII = 0x5032;
    static final int TYPE_PPM_ASCII = 0x5033;
    static final int TYPE_PBM_RAW = 0x5034;
    static final int TYPE_PGM_RAW = 0x5035;
    static final int TYPE_PPM_RAW = 0x5036;

    private BufferedImage image;

    protected PortableAnymapImageReader(ImageReaderSpi originatingProvider) {
        super(originatingProvider);
    }

    @Override
    public BufferedImage read(int imageIndex, ImageReadParam param)
            throws IOException {
        checkIndex(imageIndex);
        return read((ImageInputStream) getInput());
    }

    @Override
    public int getHeight(int imageIndex) throws IOException {
        checkIndex(imageIndex);
        return read((ImageInputStream) getInput()).getHeight();
    }

    @Override
    public int getWidth(int imageIndex) throws IOException {
        checkIndex(imageIndex);
        return read((ImageInputStream) getInput()).getWidth();
    }

    @Override
    public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException {
        checkIndex(imageIndex);
        return Collections.singletonList(
                ImageTypeSpecifier.createFromBufferedImageType(
                        read((ImageInputStream) getInput()).getType())).iterator();
    }

    @Override
    public int getNumImages(boolean allowSearch) throws IOException {
        return 1;
    }

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

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

    private static void checkIndex(int imageIndex) {
        if (imageIndex != 0) {
            throw new IndexOutOfBoundsException(
                    "A PNM file contains only one image.");
        }
    }

    private BufferedImage read(ImageInputStream in) throws IOException {
        if (image != null) {
            return image;
        }

        int type = in.readShort() & 0xffff;
        int width, height;
        int maxColorValue;
        byte[] data;

        switch (type) {
        case TYPE_PBM_ASCII:
        case TYPE_PGM_ASCII:
        case TYPE_PPM_ASCII:
            throw new IOException(
                    "An ASCII PNM format is not supported: 0x" + Integer.toHexString(type));
        case TYPE_PBM_RAW:
            width = readAsciiInt(in);
            height = readAsciiInt(in);
            maxColorValue = 1;
            data = new byte[width * height >> 3];
            in.readFully(data);
            return image = readBitmap(width, height, data);
        case TYPE_PGM_RAW:
            width = readAsciiInt(in);
            height = readAsciiInt(in);
            maxColorValue = readAsciiInt(in);
            data = new byte[width * height];
            in.readFully(data);
            return image = readGraymap(width, height, maxColorValue, data);
        case TYPE_PPM_RAW:
            width = readAsciiInt(in);
            height = readAsciiInt(in);
            maxColorValue = readAsciiInt(in);
            data = new byte[width * height * 3];
            in.readFully(data);
            return image = readPixmap(width, height, maxColorValue, data);
        default:
            throw new IOException("Unknown PNM format: " + Integer.toHexString(type));
        }
    }

    private static BufferedImage readPixmap(
            int width, int height, int maxColorValue, byte[] data) {

        BufferedImage image;
        if (maxColorValue < 256) {
            image = new BufferedImage(
                    width, height, BufferedImage.TYPE_INT_RGB);

            int r, g, b, k = 0, pixel;
            if (maxColorValue == 255) { // don't scale
                for (int y = 0; y < height; y ++) {
                    for (int x = 0; x < width && k + 3 < data.length; x ++) {
                        r = data[k ++] & 0xff;
                        g = data[k ++] & 0xff;
                        b = data[k ++] & 0xff;
                        pixel = 0xFF000000 + (r << 16) + (g << 8) + b;
                        image.setRGB(x, y, pixel);
                    }
                }
            } else {
                for (int y = 0; y < height; y ++) {
                    for (int x = 0; x < width && k + 3 < data.length; x ++) {
                        r = data[k ++] & 0xff;
                        r = (r * 255 + (maxColorValue >> 1)) / maxColorValue; // scale to 0..255 range
                        g = data[k ++] & 0xff;
                        g = (g * 255 + (maxColorValue >> 1)) / maxColorValue;
                        b = data[k ++] & 0xff;
                        b = (b * 255 + (maxColorValue >> 1)) / maxColorValue;
                        pixel = 0xFF000000 + (r << 16) + (g << 8) + b;
                        image.setRGB(x, y, pixel);
                    }
                }
            }
        } else {
            // No 16-bit per channel in Java
            image = new BufferedImage(
                    width, height, BufferedImage.TYPE_INT_RGB);

            int r, g, b, k = 0, pixel;
            for (int y = 0; y < height; y ++) {
                for (int x = 0; x < width && k + 6 < data.length; x ++) {
                    r = data[k ++] & 0xff | (data[k ++] & 0xff) << 8;
                    r = (r * 255 + (maxColorValue >> 1)) / maxColorValue; // scale to 0..255 range
                    g = data[k ++] & 0xff | (data[k ++] & 0xff) << 8;
                    g = (g * 255 + (maxColorValue >> 1)) / maxColorValue;
                    b = data[k ++] & 0xff | (data[k ++] & 0xff) << 8;
                    b = (b * 255 + (maxColorValue >> 1)) / maxColorValue;
                    pixel = 0xFF000000 + (r << 16) + (g << 8) + b;
                    image.setRGB(x, y, pixel);
                }
            }
        }

        return image;
    }

    private static BufferedImage readGraymap(
            int width, int height, int maxColorValue, byte[] data) {

        BufferedImage image;
        if (maxColorValue < 256) {
            image = new BufferedImage(width, height,
                    BufferedImage.TYPE_BYTE_GRAY);
            WritableRaster raster = image.getRaster();
            int k = 0, pixel;
            if (maxColorValue == 255) { // don't scale
                for (int y = 0; y < height; y ++) {
                    for (int x = 0; x < width && k < data.length; x ++) {
                        raster.setSample(x, y, 0, (data[k ++] & 0xff));
                    }
                }
            } else {
                for (int y = 0; y < height; y ++) {
                    for (int x = 0; x < width && k < data.length; x ++) {
                        pixel = ((data[k ++] & 0xff) * 255 + (maxColorValue >> 1)) /
                                maxColorValue; // scale to 0..255 range
                        raster.setSample(x, y, 0, pixel);
                    }
                }
            }
        } else { // 16 bit gray scale image
            image = new BufferedImage(width, height,
                    BufferedImage.TYPE_USHORT_GRAY);
            WritableRaster raster = image.getRaster();
            int k = 0, sample, pixel;
            if (maxColorValue == 65535) { // don't scale
                for (int y = 0; y < height; y ++) {
                    for (int x = 0; x < width && k < data.length - 1; x ++) {
                        sample = data[k ++] & 0xff | (data[k ++] & 0xff) << 8;
                        raster.setSample(x, y, 0, sample);
                    }
                }
            } else {
                for (int y = 0; y < height; y ++) {
                    for (int x = 0; x < width && k < data.length - 1; x ++) {
                        sample = data[k ++] & 0xff | (data[k ++] & 0xff) << 8;
                        pixel = (sample * 65535 + (maxColorValue >> 1)) / maxColorValue; // scale to 0..65535 range
                        raster.setSample(x, y, 0, pixel);
                    }
                }
            }
        }

        return image;
    }

    private static BufferedImage readBitmap(int width, int height, byte[] data) {

        BufferedImage image = new BufferedImage(
                width, height, BufferedImage.TYPE_BYTE_BINARY);

        WritableRaster raster = image.getRaster();
        int k = 0;
        int bytesPerLine = width % 8 == 0? width >> 3 : width + 8 >> 3;
        for (int y = 0; y < height; y ++) {
            for (int x = 0; x < bytesPerLine && k < data.length; x ++) {
                byte b = data[k ++];
                for (int bit = 0; bit < 8; bit ++) {
                    int xx = (x << 3) + 7 - bit;
                    if (xx < width) { // last byte in line may have padding bits
                        int pixel = (b & 1 << bit) == 0? 0xFFFFFFFF
                                : 0xFF000000; // inversion
                        raster.setSample(xx, y, 0, pixel);
                    } // else ignore padding bits
                }
            }
        }
        return image;
    }

    // ascii parser routines

    private static char readAsciiChar(ImageInputStream in) throws IOException {
        char c;
        do {
            c = (char) in.read();
            if (c == '#') { // comment : read until end of line
                do {
                    c = (char) in.read();
                } while (c != '\n' && c != '\r');
            }
        } while (c == ' ' || c == '\t' || c == '\n' || c == '\r'); // white space
        return c;
    }

    private static int readAsciiInt(ImageInputStream in) throws IOException {
        char c = readAsciiChar(in);
        if (c < '0' || '9' < c) {
            throw new IOException("Unexpected character in the PNM file.");
        }
        int i = 0;
        do {
            i = i * 10 + c - '0';
            c = (char) in.read();
        } while ('0' <= c && c <= '9');
        return i;
    }
}