package ij.plugin;

import ij.IJ;
import ij.ImagePlus;
import ij.ImageStack;
import ij.io.FileInfo;
import ij.io.OpenDialog;
import ij.process.ByteProcessor;
import ij.process.ColorProcessor;
import ij.process.ImageProcessor;
import ij.process.ShortProcessor;
import java.util.Vector;

import java.io.*;

/**
 * This plugin opens PxM format images.
 * <p/>
 * The portable graymap format is a lowest common denominator
 * grayscale file format. The definition is as follows:
 * <p/>
 * - A "magic number" for identifying the  file  type.   A  pgm
 * file's magic number is the two characters "P2".
 * - Whitespace (blanks, TABs, CRs, LFs).
 * - A width, formatted as ASCII characters in decimal.
 * - Whitespace.
 * - A height, again in ASCII decimal.
 * - Whitespace.
 * - The maximum gray value, again in ASCII decimal.
 * - Whitespace.
 * - Width * height gray values, each in ASCII decimal, between
 * 0 and the specified maximum value, separated by whi-
 * tespace, starting at the top-left corner of the graymap,
 * proceeding in normal English reading order. A value of 0
 * means black, and the maximum value means white.
 * - Characters from a "#" to the next end-of-line are ignored (comments).
 * - No line should be longer than 70 characters.
 * <p/>
 * Here is an example of a small graymap in this format:
 * P2
 * # feep.pgm
 * 24 7
 * 15
 * 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
 * 0  3  3  3  3  0  0  7  7  7  7  0  0 11 11 11 11  0  0 15 15 15 15  0
 * 0  3  0  0  0  0  0  7  0  0  0  0  0 11  0  0  0  0  0 15  0  0 15  0
 * 0  3  3  3  0  0  0  7  7  7  0  0  0 11 11 11  0  0  0 15 15 15 15  0
 * 0  3  0  0  0  0  0  7  0  0  0  0  0 11  0  0  0  0  0 15  0  0  0  0
 * 0  3  0  0  0  0  0  7  7  7  7  0  0 11 11 11 11  0  0 15  0  0  0  0
 * 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
 * <p/>
 * There is a  PGM variant that stores the pixel data as raw bytes:
 * <p/>
 * -The "magic number" is "P5" instead of "P2".
 * -The gray values are stored as plain bytes, instead of ASCII decimal.
 * -No whitespace is allowed in the grays section, and only a single
 * character of whitespace (typically a newline) is allowed after the maxval.
 * -The files are smaller and many times faster to read and write.
 * <p/>
 * Kai Barthel Nov 16 2004:
 * Extended to support PPM (portable pixmap) format images (24 bits only).
 * -The "magic numbers" are "P6" (raw) "P3" (ASCII).
 * <p/>
 * Ulf Dittmer April 2005:
 * Extended to support PBM (bitmap) images (P1 and P4)
 * <p/>
 * Jarek Sacha (jarek.at.ieee.org) December 2005:
 * Extended PPM support to 48 bit color images.
 * <p/>
 * Spencer Olson (olsonse.at.umich.edu) January 2006:
 * Extended support to stacks within PNM files.  These are simple
 * concatenations of one slice after another, where each slice is a fully
 * formatted PNM file.
 */

public class PGM_Reader extends ImagePlus implements PlugIn {

    private int width, height;
    private boolean rawBits;
    private boolean sixteenBits;
    private boolean isColor;
    private boolean isBlackWhite;
    private int maxValue;

    public void run(String arg) {
        OpenDialog od = new OpenDialog("PBM/PGM/PPM Reader...", arg);
        String directory = od.getDirectory();
        String name = od.getFileName();
        if (name == null)
            return;
        String path = directory + name;

        IJ.showStatus("Opening: " + path);
        ImageStack stack;
        try {
            stack = openFile(path);
        }
        catch (IOException e) {
            String msg = e.getMessage();
            IJ.showMessage("PBM/PGM/PPM Reader", msg.equals("") ? "" + e : msg);
            return;
        }
        setStack(name, stack);
        FileInfo fi = new FileInfo();
        fi.fileFormat = FileInfo.PGM;
        fi.directory = directory;
        fi.fileName = name;
        setFileInfo(fi);
        if (arg.equals(""))
            show();
    }

    public ImageStack openFile(String path) throws IOException {
        InputStream is = new BufferedInputStream(new FileInputStream(path));
        StreamTokenizer tok = new StreamTokenizer(is); //deprecated, but it works
        //Reader r = new BufferedReader(new InputStreamReader(is));
        //StreamTokenizer tok = new StreamTokenizer(r);  // doesn't work
        tok.resetSyntax();
        tok.wordChars(33, 255);
        tok.whitespaceChars(0, ' ');
        tok.parseNumbers();
        tok.eolIsSignificant(true);
        tok.commentChar('#');
        /* BEGIN CHANGES
         * author:  Spencer Olson
         * to enable stacks to be read from  multi-image raw files
         */
        boolean first_time = true;
        boolean tmp_sixteenBits = false;
        int tmp_width = -1, tmp_height = -1;
        ImageStack stack = null;

        do {
            try{
                openHeader(tok);
            } catch (IOException e) {
                if (first_time) {
                    /* We rethrow it if we failed on the first attempt. */
                    throw e;
                } else break;
            }

            if (first_time) {
                tmp_width  = width;
                tmp_height = height;
                tmp_sixteenBits = sixteenBits;
                stack = new ImageStack(width, height);
            } else if (width != tmp_width ||
                   height != tmp_height ||
                   sixteenBits != tmp_sixteenBits) {
                /* can only stackify layers that are simliar
                 * in size.  Perhaps we should send the
                 * user a message.
                 */
                IJ.showMessage("PBM/PGM/PPM Reader:  non-stackable multiple images found!");
                break;
            }

            /* add the next slice. */
            try{
                addImageStackSlice(is, tok, stack);
                first_time = false;
            } catch (IOException e) {
                if (first_time) {
                    /* We rethrow it if we failed on the first attempt. */
                    throw e;
                }
                /* otherwise let's let the user have what we read so far, but
                 * give an error message. */
                IJ.showMessage("PBM/PGM/PPM Reader:  Failed reading multiple image stack-slice!");
                break;
            }
        } while (true);

        /* we need to make sure that we restore these just in case: */
        width  = tmp_width;
        height = tmp_height;
        sixteenBits = tmp_sixteenBits;
        /* END CHANGES by Spencer Olson */

        return stack;
    }

    /* BEGIN CHANGES
     * author:  Spencer Olson
     * to enable stacks to be read from multi-image files
     */
    public void addImageStackSlice(InputStream is, StreamTokenizer tok, ImageStack stack) throws IOException {
        if (sixteenBits && !isColor) {
            if (rawBits) {
                ImageProcessor ip = open16bitRawImage(is, width, height);
                stack.addSlice("", ip);
                return;
            } else {
                ImageProcessor ip = open16bitAsciiImage(tok, width, height);
                stack.addSlice("", ip);
                return;
            }
        } else {
            if (!isColor) {
                byte[] pixels = new byte[width * height];
                ImageProcessor ip = new ByteProcessor(width, height, pixels, null);
                if (rawBits)
                    openRawImage(is, width * height, pixels);
                else
                    openAsciiImage(tok, width * height, pixels);

                for (int i = pixels.length - 1; i >= 0; i--) {
                    if (isBlackWhite) {
                        if (rawBits) {
                            if (i < (pixels.length / 8)) {
                                for (int bit = 7; bit >= 0; bit--) {
                                    pixels[8 * i + 7 - bit] = (byte) ((pixels[i] & ((int) Math.pow(2, bit))) == 0 ? 255 : 0);
                                }
                            }
                        } else
                            pixels[i] = (byte) (pixels[i] == 0 ? 255 : 0);
                    } else
                        pixels[i] = (byte) (0xff & (255 * (int) (0xff & pixels[i]) / maxValue));
                }
                stack.addSlice("", ip);
                return;
            } else {
                if (!sixteenBits) {
                    int[] pixels = new int[width * height];
                    byte[] bytePixels = new byte[3 * width * height];
                    ImageProcessor ip = new ColorProcessor(width, height, pixels);
                    if (rawBits)
                        openRawImage(is, 3 * width * height, bytePixels);
                    else
                        openAsciiImage(tok, 3 * width * height, bytePixels);

                    for (int i = 0; i < width * height; i++) {
                        int r = (int) (0xff & bytePixels[i * 3]);
                        int g = (int) (0xff & bytePixels[i * 3 + 1]);
                        int b = (int) (0xff & bytePixels[i * 3 + 2]);

                        r = (r * 255 / maxValue) << 16;
                        g = (g * 255 / maxValue) << 8;
                        b = (b * 255 / maxValue);
                        pixels[i] = 0xFF000000 | r | g | b;
                    }
                    stack.addSlice("", ip);
                    return;
                } else {
                    byte[] bytePixels = new byte[6 * width * height];
                    if (rawBits)
                        openRawImage(is, 6 * width * height, bytePixels);
                    else
                        openAsciiImage(tok, 6 * width * height, bytePixels);

                    short[] red = new short[width * height];
                    short[] green = new short[width * height];
                    short[] blue = new short[width * height];
                    for (int i = 0; i < width * height; i++) {
                        int r1 = 0xff & bytePixels[i * 6];
                        int r2 = 0xff & bytePixels[i * 6 + 1];
                        int g1 = 0xff & bytePixels[i * 6 + 2];
                        int g2 = 0xff & bytePixels[i * 6 + 3];
                        int b1 = 0xff & bytePixels[i * 6 + 4];
                        int b2 = 0xff & bytePixels[i * 6 + 5];

                        red[i] = (short) (0xffff & (r1 * 255 + r2));
                        green[i] = (short) (0xffff & (g1 * 255 + g2));
                        blue[i] = (short) (0xffff & (b1 * 255 + b2));
                    }
                    stack.addSlice("red", new ShortProcessor(width, height, red, null));
                    stack.addSlice("green", new ShortProcessor(width, height, green, null));
                    stack.addSlice("blue", new ShortProcessor(width, height, blue, null));
                    return;

                }
            }
        }
    }
    /* END CHANGES by Spencer Olson */

    public void openHeader(StreamTokenizer tok) throws IOException {
        String magicNumber = getWord(tok);
        if (magicNumber == null) {
            throw new IOException("PxM files must start with \"P1\" or \"P2\" or \"P3\" or \"P4\" or \"P5\" or \"P6\"");
        } else if (magicNumber.equals("P1")) {
            rawBits = false;
            isColor = false;
            isBlackWhite = true;
        } else if (magicNumber.equals("P4")) {
            rawBits = true;
            isColor = false;
            isBlackWhite = true;
        } else if (magicNumber.equals("P2")) {
            rawBits = false;
            isColor = false;
            isBlackWhite = false;
        } else if (magicNumber.equals("P5")) {
            rawBits = true;
            isColor = false;
            isBlackWhite = false;
        } else if (magicNumber.equals("P3")) {
            rawBits = false;
            isColor = true;
            isBlackWhite = false;
        } else if (magicNumber.equals("P6")) {
            rawBits = true;
            isColor = true;
            isBlackWhite = false;
        } else
            throw new IOException("PxM files must start with \"P1\" or \"P2\" or \"P3\" or \"P4\" or \"P5\" or \"P6\"");

        width = getInt(tok);
        height = getInt(tok);
        if (width == -1 || height == -1)
            throw new IOException("Error opening PxM header..");

        if (! isBlackWhite) {
            maxValue = getInt(tok);
            if (maxValue == -1)
                throw new IOException("Error opening PxM header..");
            sixteenBits = maxValue > 255;
        } else
            maxValue = 255;

        if (sixteenBits && maxValue > 65535)
            throw new IOException("The maximum gray value is larger than 65535.");
    }

    public void openAsciiImage(StreamTokenizer tok, int size, byte[] pixels) throws IOException {
        int i = 0;
        int inc = size / 20;
        while (tok.nextToken() != tok.TT_EOF) {
            if (tok.ttype == tok.TT_NUMBER) {
                pixels[i++] = (byte) (((int) tok.nval) & 255);
                if (i % inc == 0)
                    IJ.showProgress(0.5 + ((double) i / size) / 2.0);
            }
        }
        IJ.showProgress(1.0);
    }

    public void openRawImage(InputStream is, int size, byte[] pixels) throws IOException {
        int count = 0;
        while (count < size && count >= 0)
            count = is.read(pixels, count, size - count);
    }

    public ImageProcessor open16bitRawImage(InputStream is, int width, int height) throws IOException {
        int size = width * height * 2;
        byte[] bytes = new byte[size];
        int count = 0;
        while (count < size && count >= 0)
            count = is.read(bytes, count, size - count);
        short[] pixels = new short[size / 2];
        for (int i = 0, j = 0; i < size / 2; i++, j += 2)
            pixels[i] = (short) (((bytes[j] & 0xff) << 8) | (bytes[j + 1] & 0xff)); //big endian
        return new ShortProcessor(width, height, pixels, null);
    }

    public ImageProcessor open16bitAsciiImage(StreamTokenizer tok,
                                              int width, int height) throws IOException {
        int i = 0;
        int size = width * height;
        int inc = size / 20; // Progress update interval
        short[] pixels = new short[size];
        while (tok.nextToken() != tok.TT_EOF) {
            if (tok.ttype == tok.TT_NUMBER) {
                pixels[i++] = (short) (((int) tok.nval) & 65535);
                if (i % inc == 0)
                    IJ.showProgress(0.5 + ((double) i / size) / 2.0);
            }
        }
        IJ.showProgress(1.0);
        return new ShortProcessor(width, height, pixels, null);
    }

    String getWord(StreamTokenizer tok) throws IOException {
        while (tok.nextToken() != tok.TT_EOF) {
            if (tok.ttype == tok.TT_WORD)
                return tok.sval;
        }
        return null;
    }

    int getInt(StreamTokenizer tok) throws IOException {
        while (tok.nextToken() != tok.TT_EOF) {
            if (tok.ttype == tok.TT_NUMBER)
                return (int) tok.nval;
        }
        return -1;
    }

}