/*
 * $RCSfile: PNMImageWriter.java,v $
 *
 * 
 * Copyright (c) 2005 Sun Microsystems, Inc. All  Rights Reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met: 
 * 
 * - Redistribution of source code must retain the above copyright 
 *   notice, this  list of conditions and the following disclaimer.
 * 
 * - Redistribution 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 of Sun Microsystems, Inc. or the names of 
 * contributors may be used to endorse or promote products derived 
 * from this software without specific prior written permission.
 * 
 * This software is provided "AS IS," without a warranty of any 
 * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND 
 * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY
 * EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL 
 * NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF 
 * USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR 
 * ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL,
 * CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND
 * REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR
 * INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGES. 
 * 
 * You acknowledge that this software is not designed or intended for 
 * use in the design, construction, operation or maintenance of any 
 * nuclear facility. 
 *
 * $Revision: 1.1 $
 * $Date: 2005-02-11 05:01:40 $
 * $State: Exp $
 */
package com.sun.media.imageioimpl.plugins.pnm;

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.color.ColorSpace;
import java.awt.image.ColorModel;
import java.awt.image.ComponentSampleModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.IndexColorModel;
import java.awt.image.MultiPixelPackedSampleModel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;

import java.io.IOException;

import java.util.Iterator;

import javax.imageio.IIOImage;
import javax.imageio.IIOException;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.sun.media.imageio.plugins.pnm.PNMImageWriteParam;
import com.sun.media.imageioimpl.common.ImageUtil;
/**
 * The Java Image IO plugin writer for encoding a binary RenderedImage into
 * a PNM format.
 *
 * The encoding process may clip, subsample using the parameters
 * specified in the <code>ImageWriteParam</code>.
 *
 * @see com.sun.media.imageio.plugins.PNMImageWriteParam
 */
public class PNMImageWriter extends ImageWriter {
    private static final int PBM_ASCII  = '1';
    private static final int PGM_ASCII  = '2';
    private static final int PPM_ASCII  = '3';
    private static final int PBM_RAW    = '4';
    private static final int PGM_RAW    = '5';
    private static final int PPM_RAW    = '6';

    private static final int SPACE      = ' ';

    private static final String COMMENT =
        "# written by com.sun.media.imageioimpl.PNMImageWriter";

    private static byte[] lineSeparator;

    private int variant;
    private int maxValue;

    static {
        if (lineSeparator == null) {
            String ls = (String)java.security.AccessController.doPrivileged(
               new sun.security.action.GetPropertyAction("line.separator"));
            lineSeparator = ls.getBytes();
        }
    }

    /** The output stream to write into */
    private ImageOutputStream stream = null;

    /** Constructs <code>PNMImageWriter</code> based on the provided
     *  <code>ImageWriterSpi</code>.
     */
    public PNMImageWriter(ImageWriterSpi originator) {
        super(originator);
    }

    public void setOutput(Object output) {
        super.setOutput(output); // validates output
        if (output != null) {
            if (!(output instanceof ImageOutputStream))
                throw new IllegalArgumentException(I18N.getString("PNMImageWriter0"));
            this.stream = (ImageOutputStream)output;
        } else
            this.stream = null;
    }

    public ImageWriteParam getDefaultWriteParam() {
        return new PNMImageWriteParam();
    }

    public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) {
        return null;
    }

    public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType,
                                               ImageWriteParam param) {
        return new PNMMetadata(imageType, param);
    }

    public IIOMetadata convertStreamMetadata(IIOMetadata inData,
                                             ImageWriteParam param) {
        return null;
    }

    public IIOMetadata convertImageMetadata(IIOMetadata inData,
                                            ImageTypeSpecifier imageType,
                                            ImageWriteParam param) {
        // Check arguments.
        if(inData == null) {
            throw new IllegalArgumentException("inData == null!");
        }
        if(imageType == null) {
            throw new IllegalArgumentException("imageType == null!");
        }

        PNMMetadata outData = null;

        // Obtain a PNMMetadata object.
        if(inData instanceof PNMMetadata) {
            // Clone the input metadata.
            outData = (PNMMetadata)((PNMMetadata)inData).clone();
        } else {
            try {
                outData = new PNMMetadata(inData);
            } catch(IIOInvalidTreeException e) {
                // XXX Warning
                outData = new PNMMetadata();
            }
        }

        // Update the metadata per the image type and param.
        outData.initialize(imageType, param);

        return outData;
    }

    public boolean canWriteRasters() {
        return true;
    }

    public void write(IIOMetadata streamMetadata,
                      IIOImage image,
                      ImageWriteParam param) throws IOException {
        clearAbortRequest();
        processImageStarted(0);
        if (param == null)
            param = getDefaultWriteParam();

        RenderedImage input = null;
        Raster inputRaster = null;
        boolean writeRaster = image.hasRaster();
        Rectangle sourceRegion = param.getSourceRegion();
        SampleModel sampleModel = null;
        ColorModel colorModel = null;

        if (writeRaster) {
            inputRaster = image.getRaster();
            sampleModel = inputRaster.getSampleModel();
            if (sourceRegion == null)
                sourceRegion = inputRaster.getBounds();
            else
                sourceRegion = sourceRegion.intersection(inputRaster.getBounds());
        } else {
            input = image.getRenderedImage();
            sampleModel = input.getSampleModel();
            colorModel = input.getColorModel();
            Rectangle rect = new Rectangle(input.getMinX(), input.getMinY(),
                                           input.getWidth(), input.getHeight());
            if (sourceRegion == null)
                sourceRegion = rect;
            else
                sourceRegion = sourceRegion.intersection(rect);
        }

        if (sourceRegion.isEmpty())
            throw new RuntimeException(I18N.getString("PNMImageWrite1"));

	ImageUtil.canEncodeImage(this, colorModel, sampleModel);

        int scaleX = param.getSourceXSubsampling();
        int scaleY = param.getSourceYSubsampling();
        int xOffset = param.getSubsamplingXOffset();
        int yOffset = param.getSubsamplingYOffset();

        sourceRegion.translate(xOffset, yOffset);
        sourceRegion.width -= xOffset;
        sourceRegion.height -= yOffset;

        int minX = sourceRegion.x / scaleX;
        int minY = sourceRegion.y / scaleY;
        int w = (sourceRegion.width + scaleX - 1) / scaleX;
        int h = (sourceRegion.height + scaleY - 1) / scaleY;

        Rectangle destinationRegion = new Rectangle(minX, minY, w, h);

        int tileHeight = sampleModel.getHeight();
        int tileWidth = sampleModel.getWidth();

        // Raw data can only handle bytes, everything greater must be ASCII.
        int[] sampleSize = sampleModel.getSampleSize();
        int[] sourceBands = param.getSourceBands();
        boolean noSubband = true;
        int numBands = sampleModel.getNumBands();

        if (sourceBands != null) {
            sampleModel = sampleModel.createSubsetSampleModel(sourceBands);
            colorModel = null;
            noSubband = false;
            numBands = sampleModel.getNumBands();
        } else {
            sourceBands = new int[numBands];
            for (int i = 0; i < numBands; i++)
                sourceBands[i] = i;
        }

        // Colormap populated for non-bilevel IndexColorModel only.
        byte[] reds = null;
        byte[] greens = null;
        byte[] blues = null;

        // Flag indicating that PB data should be inverted before writing.
        boolean isPBMInverted = false;

        if (numBands == 1) {
            if (colorModel instanceof IndexColorModel) {
                IndexColorModel icm = (IndexColorModel)colorModel;

                int mapSize = icm.getMapSize();
                if (mapSize < (1 << sampleSize[0]))
                    throw new RuntimeException(I18N.getString("PNMImageWrite2"));

                if(sampleSize[0] == 1) {
                    variant = PBM_RAW;

                    // Set PBM inversion flag if 1 maps to a higher color
                    // value than 0: PBM expects white-is-zero so if this
                    // does not obtain then inversion needs to occur.
                    isPBMInverted = icm.getRed(1) > icm.getRed(0);
                } else {
                    variant = PPM_RAW;

                    reds = new byte[mapSize];
                    greens = new byte[mapSize];
                    blues = new byte[mapSize];

                    icm.getReds(reds);
                    icm.getGreens(greens);
                    icm.getBlues(blues);
                }
            } else if (sampleSize[0] == 1) {
                variant = PBM_RAW;
            } else if (sampleSize[0] <= 8) {
                variant = PGM_RAW;
            } else {
                variant = PGM_ASCII;
            }
        } else if (numBands == 3) {
            if (sampleSize[0] <= 8 && sampleSize[1] <= 8 &&
                sampleSize[2] <= 8) {	// all 3 bands must be <= 8
                variant = PPM_RAW;
            } else {
                variant = PPM_ASCII;
            }
        } else {
            throw new RuntimeException(I18N.getString("PNMImageWrite3"));
        }

        IIOMetadata inputMetadata = image.getMetadata();
        ImageTypeSpecifier imageType;
        if(colorModel != null) {
            imageType = new ImageTypeSpecifier(colorModel, sampleModel);
        } else {
            int dataType = sampleModel.getDataType();
            switch(numBands) {
            case 1:
                imageType =
                    ImageTypeSpecifier.createGrayscale(sampleSize[0], dataType,
                                                       false);
                break;
            case 3:
                ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
                imageType =
                    ImageTypeSpecifier.createInterleaved(cs,
                                                         new int[] {0, 1, 2},
                                                         dataType,
                                                         false, false);
                break;
            default:
                throw new IIOException("Cannot encode image with "+
                                       numBands+" bands!");
            }
        }

        PNMMetadata metadata;
        if(inputMetadata != null) {
            // Convert metadata.
            metadata = (PNMMetadata)convertImageMetadata(inputMetadata,
                                                         imageType, param);
        } else {
            // Use default.
            metadata = (PNMMetadata)getDefaultImageMetadata(imageType, param);
        }

        // Read parameters
        boolean isRawPNM;
        if(param instanceof PNMImageWriteParam) {
            isRawPNM = ((PNMImageWriteParam)param).getRaw();
        } else {
            isRawPNM = metadata.isRaw();
        }

        maxValue = metadata.getMaxValue();
        for (int i = 0; i < sampleSize.length; i++) {
            int v = (1 << sampleSize[i]) - 1;
            if (v > maxValue) {
                maxValue = v;
            }
        }

        if (isRawPNM) {
            // Raw output is desired.
            int maxBitDepth = metadata.getMaxBitDepth();
            if (!isRaw(variant) && maxBitDepth <= 8) {
                // Current variant is ASCII and the bit depth is acceptable
                // so convert to RAW variant by adding '3' to variant.
                variant += 0x3;
            } else if(isRaw(variant) && maxBitDepth > 8) {
                // Current variant is RAW and the bit depth it too large for
                // RAW so convert to ASCII.
                variant -= 0x3;
            }
            // Omitted cases are (variant == RAW && max <= 8) and
            // (variant == ASCII && max > 8) neither of which requires action.
        } else if(isRaw(variant)) {
            // Raw output is NOT desired so convert to ASCII
            variant -= 0x3;
        }

        // Write PNM file.
        stream.writeByte('P');			// magic value: 'P'
        stream.writeByte(variant);

        stream.write(lineSeparator);
        stream.write(COMMENT.getBytes());	// comment line

        // Write the comments provided in the metadata
        Iterator comments = metadata.getComments();
        if(comments != null) {
            while(comments.hasNext()) {
                stream.write(lineSeparator);
                String comment = "# " + (String)comments.next();
                stream.write(comment.getBytes());
            }
        }

        stream.write(lineSeparator);
        writeInteger(stream, w);		// width
        stream.write(SPACE);
        writeInteger(stream, h);		// height

        // Write sample max value for non-binary images
        if ((variant != PBM_RAW) && (variant != PBM_ASCII)) {
            stream.write(lineSeparator);
            writeInteger(stream, maxValue);
        }

        // The spec allows a single character between the
        // last header value and the start of the raw data.
        if (variant == PBM_RAW ||
            variant == PGM_RAW ||
            variant == PPM_RAW) {
            stream.write('\n');
        }

        // Set flag for optimal image writing case: row-packed data with
        // correct band order if applicable.
        boolean writeOptimal = false;
        if (variant == PBM_RAW &&
            sampleModel.getTransferType() == DataBuffer.TYPE_BYTE &&
            sampleModel instanceof MultiPixelPackedSampleModel) {

            MultiPixelPackedSampleModel mppsm =
                (MultiPixelPackedSampleModel)sampleModel;

            int originX = 0;
            if (writeRaster)
                originX = inputRaster.getMinX();
            else
                originX = input.getMinX();

            // Must have left-aligned bytes with unity bit stride.
            if(mppsm.getBitOffset((sourceRegion.x - originX) % tileWidth) == 0 &&
               mppsm.getPixelBitStride() == 1 && scaleX == 1)
                writeOptimal = true;
        } else if ((variant == PGM_RAW || variant == PPM_RAW) &&
                   sampleModel instanceof ComponentSampleModel &&
                   !(colorModel instanceof IndexColorModel)) {

            ComponentSampleModel csm =
                (ComponentSampleModel)sampleModel;

            // Pixel stride must equal band count.
            if(csm.getPixelStride() == numBands && scaleX == 1) {
                writeOptimal = true;

                // Band offsets must equal band indices.
                if(variant == PPM_RAW) {
                    int[] bandOffsets = csm.getBandOffsets();
                    for(int b = 0; b < numBands; b++) {
                        if(bandOffsets[b] != b) {
                            writeOptimal = false;
                            break;
                        }
                    }
                }
            }
        }

        // Write using an optimal approach if possible.
        if(writeOptimal) {
            int bytesPerRow = variant == PBM_RAW ?
                (w + 7)/8 : w * sampleModel.getNumBands();
            byte[] bdata = null;
            byte[] invertedData = new byte[bytesPerRow];

            // Loop over tiles to minimize cobbling.
            for(int j = 0; j < sourceRegion.height; j++) {
                if (abortRequested())
                    break;
                Raster lineRaster = null;
                if (writeRaster) {
                    lineRaster = inputRaster.createChild(sourceRegion.x,
                                                         j,
                                                         sourceRegion.width,
                                                         1, 0, 0, null);
                } else {
                    lineRaster =
                        input.getData(new Rectangle(sourceRegion.x,
                                                    sourceRegion.y + j,
                                                    w, 1));
                    lineRaster = lineRaster.createTranslatedChild(0, 0);
                }

                bdata = ((DataBufferByte)lineRaster.getDataBuffer()).getData();

                sampleModel = lineRaster.getSampleModel();
                int offset = 0;
                if (sampleModel instanceof ComponentSampleModel) {
                    offset =
                        ((ComponentSampleModel)sampleModel).getOffset(lineRaster.getMinX()-lineRaster.getSampleModelTranslateX(),
                                                                      lineRaster.getMinY()-lineRaster.getSampleModelTranslateY());
                } else if (sampleModel instanceof MultiPixelPackedSampleModel) {
                    offset = ((MultiPixelPackedSampleModel)sampleModel).getOffset(lineRaster.getMinX() -
                                                                        lineRaster.getSampleModelTranslateX(),
                                                                      lineRaster.getMinX()-lineRaster.getSampleModelTranslateY());
                }

		if (isPBMInverted) {
                    for(int k = offset, m = 0; m < bytesPerRow; k++, m++)
                        invertedData[m] = (byte)~bdata[k];
                    bdata = invertedData;
                    offset = 0;
                }

                stream.write(bdata, offset, bytesPerRow);
                processImageProgress(100.0F * j / sourceRegion.height);
            }

            // Write all buffered bytes and return.
            stream.flush();
            if (abortRequested())
                processWriteAborted();
            else
                processImageComplete();
            return;
        }

        // Buffer for 1 rows of original pixels
        int size = sourceRegion.width * numBands;

        int[] pixels = new int[size];

        // Also allocate a buffer to hold the data to be written to the file,
        // so we can use array writes.
        byte[] bpixels =
            reds == null ? new byte[w * numBands] : new byte[w * 3];

        // The index of the sample being written, used to
        // place a line separator after every 16th sample in
        // ASCII mode.  Not used in raw mode.
        int count = 0;

        // Process line by line
        int lastRow = sourceRegion.y + sourceRegion.height;

        for (int row = sourceRegion.y; row < lastRow; row += scaleY) {
            if (abortRequested())
                break;
            // Grab the pixels
            Raster src = null;

            if (writeRaster)
                src = inputRaster.createChild(sourceRegion.x,
                                              row,
                                              sourceRegion.width, 1,
                                              sourceRegion.x, row, sourceBands);
            else
                src = input.getData(new Rectangle(sourceRegion.x, row,
                                                  sourceRegion.width, 1));
            src.getPixels(sourceRegion.x, row, sourceRegion.width, 1, pixels);

            if (isPBMInverted)
                for (int i = 0; i < size; i += scaleX)
                    bpixels[i] ^= 1;

            switch (variant) {
            case PBM_ASCII:
            case PGM_ASCII:
                for (int i = 0; i < size; i += scaleX) {
                    if ((count++ % 16) == 0)
                        stream.write(lineSeparator);
                    else
                        stream.write(SPACE);

                    writeInteger(stream, pixels[i]);
                }
                stream.write(lineSeparator);
                break;

            case PPM_ASCII:
                if (reds == null) {	// no need to expand
                    int[] bandOffset =
                        ((ComponentSampleModel)sampleModel).getBandOffsets();
                    for (int i = 0; i < size; i += scaleX * numBands) {
                        for (int j = 0; j < numBands; j++) {
                            if ((count++ % 16) == 0)
                                stream.write(lineSeparator);
                            else
                                stream.write(SPACE);

                            writeInteger(stream, pixels[i + j]);
                        }
                    }
                } else {
                    for (int i = 0; i < size; i += scaleX) {
                        if ((count++ % 5) == 0)
                            stream.write(lineSeparator);
                        else
                            stream.write(SPACE);

                        writeInteger(stream, (reds[pixels[i]] & 0xFF));
                        stream.write(SPACE);
                        writeInteger(stream, (greens[pixels[i]] & 0xFF));
                        stream.write(SPACE);
                        writeInteger(stream, (blues[pixels[i]] & 0xFF));
                    }
                }
                stream.write(lineSeparator);
                break;

            case PBM_RAW:
                // 8 pixels packed into 1 byte, the leftovers are padded.
                int kdst = 0;
                int ksrc = 0;
                int b = 0;
                int pos = 7;
                for (int i = 0; i < size; i += scaleX) {
                    b |= pixels[i] << pos;
                    pos--;
                    if (pos == -1) {
                        bpixels[kdst++] = (byte)b;
                        b = 0;
                        pos = 7;
                    }
                }

                if (pos != 7)
                    bpixels[kdst++] = (byte)b;

                stream.write(bpixels, 0, kdst);
                break;

            case PGM_RAW:
                for (int i = 0, j = 0; i < size; i += scaleX) {
                    bpixels[j++] = (byte)(pixels[i]);
                }
                stream.write(bpixels, 0, w);
                break;

            case PPM_RAW:
                if (reds == null) {	// no need to expand
                    for (int i = 0, k = 0; i < size; i += scaleX * numBands) {
                        for (int j = 0; j < numBands; j++)
                          bpixels[k++] = (byte)(pixels[i + j] & 0xFF);
                    }
                } else {
                    for (int i = 0, j = 0; i < size; i += scaleX) {
                        bpixels[j++] = reds[pixels[i]];
                        bpixels[j++] = greens[pixels[i]];
                        bpixels[j++] = blues[pixels[i]];
                    }
                }
                stream.write(bpixels, 0, bpixels.length);
                break;
            }

            processImageProgress(100.0F * (row - sourceRegion.y) /
                                 sourceRegion.height);
        }

        // Force all buffered bytes to be written out.
        stream.flush();

        if (abortRequested())
            processWriteAborted();
        else
            processImageComplete();
    }

    public void reset() {
        super.reset();
        stream = null;
    }

    /** Writes an integer to the output in ASCII format. */
    private void writeInteger(ImageOutputStream output, int i) throws IOException {
        output.write(Integer.toString(i).getBytes());
    }

    /** Writes a byte to the output in ASCII format. */
    private void writeByte(ImageOutputStream output, byte b) throws IOException {
        output.write(Byte.toString(b).getBytes());
    }

    /** Returns true if file variant is raw format, false if ASCII. */
    private boolean isRaw(int v) {
        return (v >= PBM_RAW);
    }
}
