/*
 * @(#)JPEGImageWriter.java	1.17 01/03/23
 *
 * Copyright 2000 by Sun Microsystems, Inc.,
 * 901 San Antonio Road, Palo Alto, California, 94303, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information
 * of Sun Microsystems, Inc. ("Confidential Information").  You
 * shall not disclose such Confidential Information and shall use
 * it only in accordance with the terms of the license agreement
 * you entered into with Sun.
 */

package com.sun.imageio.plugins.jpeg;

import javax.imageio.IIOException;
import javax.imageio.ImageWriter;
import javax.imageio.ImageWriteParam;
import javax.imageio.IIOImage;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
import javax.imageio.plugins.jpeg.JPEGQTable;
import javax.imageio.plugins.jpeg.JPEGHuffmanTable;

import org.w3c.dom.Node;

import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.awt.image.SampleModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.ColorConvertOp;
import java.awt.image.RenderedImage;
import java.awt.image.BufferedImage;
import java.awt.color.ColorSpace;
import java.awt.Dimension;
import java.awt.Rectangle;

import java.io.IOException;

import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;

public class JPEGImageWriter extends ImageWriter {

    ///////// Private variables

    private boolean debug = false;

    private ImageOutputStream ios = null;

    /** The Raster we will write from */
    private Raster srcRas = null;
    
    /** An intermediate Raster holding compressor-friendly data */
    private WritableRaster raster = null;

    /** 
     * Set to true if we are writing an image with an 
     * indexed ColorModel 
     */
    private boolean indexed = false;
    private IndexColorModel indexCM = null;

    private boolean convertTosRGB = false;  // Used by PhotoYCC only
    private WritableRaster converted = null;
    
    private int sourceXOffset = 0;
    private int sourceYOffset = 0;
    private int sourceWidth = 0;
    private int [] srcBands = null;
    private int sourceHeight = 0;

    /** Used when calling listeners */
    private int currentImage = 0;

    private static String [] warnings = {
       "Source image type not convertible to destination type. " 
       + "Destination type ignored.",
       "Stream metadata ignored on write",
       "Metadata component ids incompatible with destination type. " 
       + "Metadata modified.",
       "Metadata JFIF settings incompatible with destination type. " 
       + "Metadata modified.",
       "Metadata Adobe settings incompatible with destination type. "
       + "Metadata modified.",
       "Metadata JFIF settings incompatible with image type. " 
       + "Metadata modified.",
       "Metadata Adobe settings incompatible with image type. "
       + "Metadata modified.",
       "Metadata must be JPEGMetadata when writing a Raster. "
       + "Metadata ignored.",
       "Band subset not allowed for an IndexColorModel image.  "
       + "Band subset ignored.",
    };

    private ColorConvertOp convertOp = null;

    private JPEGQTable [] streamQTables = null;
    private JPEGHuffmanTable[] streamDCHuffmanTables = null;
    private JPEGHuffmanTable[] streamACHuffmanTables = null;

    // Parameters for writing metadata
    private boolean ignoreJFIF = false;  // If it's there, use it
    private boolean ignoreAdobe = false;  // If it's there, use it
    private int newAdobeTransform = JPEG.ADOBE_IMPOSSIBLE;  // Change if needed
    private boolean writeDefaultJFIF = false;
    private boolean writeAdobe = false;
    private JPEGMetadata metadata = null;
    
    private boolean sequencePrepared = false;

    private int numScans = 0;

    ///////// End of Private variables

    ///////// Protected variables

    protected static final int WARNING_NO_CONVERSION = 0;
    protected static final int WARNING_STREAM_METADATA_IGNORED = 1;
    protected static final int WARNING_DEST_METADATA_COMP_MISMATCH = 2;
    protected static final int WARNING_DEST_METADATA_JFIF_MISMATCH = 3;
    protected static final int WARNING_DEST_METADATA_ADOBE_MISMATCH = 4;
    protected static final int WARNING_IMAGE_METADATA_JFIF_MISMATCH = 5;
    protected static final int WARNING_IMAGE_METADATA_ADOBE_MISMATCH = 6;
    protected static final int WARNING_METADATA_NOT_JPEG_FOR_RASTER = 7;
    protected static final int WARNING_NO_BANDS_ON_INDEXED = 8;

    ///////// End of Protected variables
    
    ///////// static initializer
    
    static {
        java.security.AccessController.doPrivileged(
            new sun.security.action.LoadLibraryAction("jpeg"));
        initWriterIDs(ImageOutputStream.class, 
                      JPEGQTable.class, 
                      JPEGHuffmanTable.class);
    }

    //////// Public API

    public JPEGImageWriter(ImageWriterSpi originator) {
        super(originator);
        // Synchronize to protect the native linked list
        synchronized (JPEGImageReader.class) {
            initJPEGImageWriter();
        }
    }


    public void setOutput(Object output) {
        super.setOutput(output); // validates output
        ios = (ImageOutputStream) output; // so this will always work
        // Set the native destination
        // Synchronize to protect the native linked list
        synchronized (JPEGImageReader.class) {
            setDest(ios);
        }
        srcRas = null;
        raster = null;
        currentImage = 0;
        System.gc();
    }

    public ImageWriteParam getDefaultWriteParam() {
        return new JPEGImageWriteParam(null);
    }

    public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) {
        return new JPEGMetadata(param, this);
    }

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

    public IIOMetadata convertStreamMetadata(IIOMetadata inData,
                                             ImageWriteParam param) {
        // There isn't much we can do.  If it's one of ours, then
        // return it.  Otherwise just return null.  We use it only
        // for tables, so we can't get a default and modify it,
        // as this will usually not be what is intended.
        if (inData instanceof JPEGMetadata) {
            JPEGMetadata jpegData = (JPEGMetadata) inData;
            if (jpegData.isStream) {
                return inData;
            }
        }
        return null;
    }

    public IIOMetadata
        convertImageMetadata(IIOMetadata inData,
                             ImageTypeSpecifier imageType,
                             ImageWriteParam param) {
        // If it's one of ours, just return it
        if (inData instanceof JPEGMetadata) {
            JPEGMetadata jpegData = (JPEGMetadata) inData;
            if (!jpegData.isStream) {
                return inData;
            } else {
                // Can't convert stream metadata to image metadata
                // XXX Maybe this should put out a warning?
                return null;
            }
        }
        // If it's not one of ours, create a default and set it from
        // the standard tree from the input, if it exists.
        if (inData.isStandardMetadataFormatSupported()) {
            String formatName = 
                IIOMetadataFormatImpl.standardMetadataFormatName;
            Node tree = inData.getAsTree(formatName);
            if (tree != null) {
                JPEGMetadata jpegData = new JPEGMetadata(imageType,
                                                         param,
                                                         this);
                try {
                    jpegData.setFromTree(formatName, tree);
                } catch (IIOInvalidTreeException e) {
                    // Other plug-in generates bogus standard tree
                    // XXX Maybe this should put out a warning?
                    return null;
                }

                return jpegData;
            }
        }
            return null;
    }

    /*
    public int getNumThumbnailsSupported(ImageTypeSpecifier imageType,
                                         ImageWriteParam param,
                                         IIOMetadata streamMetadata,
                                         IIOMetadata imageMetadata) {
        // FIXME - implement
        // If the image type and metadata are JFIF compatible, return
        // a big number.  How big? MAX_INT makes sense, although maybe
        // there is something smaller that is more sensible.
        return 0;
    }

    public Dimension[] getPreferredThumbnailSizes(ImageTypeSpecifier imageType,
                                                  ImageWriteParam param,
                                                  IIOMetadata streamMetadata,
                                                  IIOMetadata imageMetadata) {
        // FIXME - implement
        // Again, check whether the image type and metadata are JFIF 
        // compatible, and if so, return the relevant range.  Width and
        // height are constrained to 8 bits.
        throw new RuntimeException("Unimplemented");
    }
    */

    public boolean canWriteRasters() {
        return true;
    }

    public void write(IIOMetadata streamMetadata,
                      IIOImage image,
                      ImageWriteParam param) throws IOException {

        if (ios == null) {
            throw new IllegalStateException("Output has not been set!");
        }

        // FIXME - thumbnails
        
        // if streamMetadata is not null, issue a warning
        if (streamMetadata != null) {
            warningOccurred(WARNING_STREAM_METADATA_IGNORED);
        }

        // Obtain the raster and image, if there is one
        boolean rasterOnly = image.hasRaster();

        RenderedImage rimage = null;
        if (rasterOnly) {
            srcRas = image.getRaster();
        } else {
            rimage = image.getRenderedImage();
            if (rimage instanceof BufferedImage) {
                srcRas = ((BufferedImage)rimage).getRaster();
            } else {
                // XXX - this makes a copy, which is memory-inefficient
                srcRas = rimage.getData();
            }
        }

        // Now determine if we are using a band subset

        // By default, we are using all source bands
        int numSrcBands = srcRas.getNumBands();
        indexed = false;
        indexCM = null;
        ColorModel cm = null;
        if (!rasterOnly) {
            cm = rimage.getColorModel();
            if (cm != null) {
                if (cm instanceof IndexColorModel) {
                    indexed = true;
                    indexCM = (IndexColorModel) cm;
                    numSrcBands = cm.getNumComponents();
                }
            }
        }

        srcBands = JPEG.bandOffsets[numSrcBands-1];
        int numBandsUsed = numSrcBands;
        // Consult the param to determine if we're writing a subset
        
        if (param != null) {
            int[] sBands = param.getSourceBands();
            if (sBands != null) {
                if (indexed) {
                    warningOccurred(WARNING_NO_BANDS_ON_INDEXED);
                } else {
                    srcBands = sBands;
                    numBandsUsed = srcBands.length;
                    if (numBandsUsed > numSrcBands) {
                        throw new IIOException
                        ("ImageWriteParam specifies too many source bands");
                    }
                }
            }
        }

        boolean usingBandSubset = (numBandsUsed != numSrcBands);
        boolean fullImage = ((!rasterOnly) && (!usingBandSubset));

        int [] bandSizes = null;
        if (!indexed) {
            bandSizes = srcRas.getSampleModel().getSampleSize();
            // If this is a subset, we must adjust bandSizes
            if (usingBandSubset) {
                int [] temp = new int [numBandsUsed];
                for (int i = 0; i < numBandsUsed; i++) {
                    temp[i] = bandSizes[srcBands[i]];
                }
                bandSizes = temp;
            }
        } else {
            int [] tempSize = srcRas.getSampleModel().getSampleSize();
            bandSizes = new int [numSrcBands];
            for (int i = 0; i < numSrcBands; i++) {
                bandSizes[i] = tempSize[0];  // All the same
            }
        }

        if (debug) {
            System.out.println("numSrcBands is " + numSrcBands);
            System.out.println("numBandsUsed is " + numBandsUsed);
            System.out.println("usingBandSubset is " + usingBandSubset);
            System.out.println("fullImage is " + fullImage);
            System.out.print("Band sizes:");
            for (int i = 0; i< bandSizes.length; i++) {
                System.out.print(" " + bandSizes[i]);
            }
            System.out.println();
        }

        // Destination type, if there is one
        ImageTypeSpecifier destType = null;
        if (param != null) {
            destType = param.getDestinationType();
            if ((fullImage) && (destType != null)) {
                warningOccurred(WARNING_NO_CONVERSION);
                destType = null;
            }
        }

        // Examine the param

        sourceXOffset = 0;
        sourceYOffset = 0;
        int imageWidth = srcRas.getWidth();
        int imageHeight = srcRas.getHeight();
        sourceWidth = imageWidth;
        sourceHeight = imageHeight;
        int periodX = 1;
        int periodY = 1;
        int gridX = 0;
        int gridY = 0;
        JPEGQTable [] qTables = null;
        JPEGHuffmanTable[] DCHuffmanTables = null;
        JPEGHuffmanTable[] ACHuffmanTables = null;
        boolean optimizeHuffman = false;
        JPEGImageWriteParam jparam = null;
        int progressiveMode = ImageWriteParam.MODE_DISABLED;

        if (param != null) {

            Rectangle sourceRegion = param.getSourceRegion();
            if (sourceRegion != null) {
                sourceXOffset = sourceRegion.x;
                sourceYOffset = sourceRegion.y;
                sourceWidth = sourceRegion.width;
                sourceHeight = sourceRegion.height;
            }

            if (sourceWidth + sourceXOffset > imageWidth) {
                sourceWidth = imageWidth - sourceXOffset;
            }
            if (sourceHeight + sourceYOffset > imageHeight) {
                sourceHeight = imageHeight - sourceYOffset;
            }

            periodX = param.getSourceXSubsampling();
            periodY = param.getSourceYSubsampling();
            gridX = param.getSubsamplingXOffset();
            gridY = param.getSubsamplingYOffset();

            switch(param.getCompressionMode()) {
            case ImageWriteParam.MODE_DISABLED:
                throw new IIOException("JPEG compression cannot be disabled");
            case ImageWriteParam.MODE_EXPLICIT:
                float quality = param.getCompressionQuality();
                qTables = new JPEGQTable[2];
                qTables[0] = JPEGQTable.K1Div2Luminance.getScaledInstance
                    (quality, true);
                qTables[1] = JPEGQTable.K2Div2Chrominance.getScaledInstance
                    (quality, true);
                break;
            case ImageWriteParam.MODE_DEFAULT:
                qTables = new JPEGQTable[2];
                qTables[0] = JPEGQTable.K1Div2Luminance;
                qTables[1] = JPEGQTable.K2Div2Chrominance;
                break;
            // We'll handle the metadata case later
            }

            progressiveMode = param.getProgressiveMode();

            if (param instanceof JPEGImageWriteParam) {
                jparam = (JPEGImageWriteParam)param;                
                optimizeHuffman = jparam.getOptimizeHuffmanTables();
            }
        }

        // Now examine the metadata
        IIOMetadata mdata = image.getMetadata();
        if (mdata != null) {
            if (mdata instanceof JPEGMetadata) {
                metadata = (JPEGMetadata) mdata;
                if (debug) {
                    System.out.println
                        ("We have metadata, and it's JPEG metadata");
                }
            } else {
                if (!rasterOnly) {
                    ImageTypeSpecifier type = destType;
                    if (type == null) {
                        type = new ImageTypeSpecifier(rimage);
                    }
                    metadata = (JPEGMetadata) convertImageMetadata(mdata,
                                                                   type,
                                                                   param);
                } else {
                    warningOccurred(WARNING_METADATA_NOT_JPEG_FOR_RASTER);
                }                            
            }
        }

        // First set a default state

        ignoreJFIF = false;  // If it's there, use it
        ignoreAdobe = false;  // If it's there, use it
        newAdobeTransform = JPEG.ADOBE_IMPOSSIBLE;  // Change if needed
        writeDefaultJFIF = false;
        writeAdobe = false;

        // By default we'll do no conversion:
        int inCsType = JPEG.JCS_UNKNOWN;
        int outCsType = JPEG.JCS_UNKNOWN;

        JFIFMarkerSegment jfif = null;
        AdobeMarkerSegment adobe = null;
        SOFMarkerSegment sof = null;

        if (metadata != null) {
            jfif = (JFIFMarkerSegment) metadata.findMarkerSegment
                (JFIFMarkerSegment.class, true);
            adobe = (AdobeMarkerSegment) metadata.findMarkerSegment
                (AdobeMarkerSegment.class, true);
            sof = (SOFMarkerSegment) metadata.findMarkerSegment
                (SOFMarkerSegment.class, true);
        }

        convertTosRGB = false;  // PhotoYCC does this
        converted = null;

        if (destType != null) {
            if (numBandsUsed != destType.getNumBands()) {
                throw new IIOException
                    ("Number of source bands != number of destination bands");
            }
            // Check the metadata against the destination type
            if (metadata != null) {
                checkSOFBands(sof, numBandsUsed);

                checkJFIF(jfif, destType, false);
                checkAdobe(adobe, destType, false);

            } else { // no metadata, but there is a dest type
                // If we can add a JFIF or an Adobe marker segment, do so
                if (JPEG.isJFIFcompliant(destType, false)) {
                    writeDefaultJFIF = true;
                } else {
                    int transform = JPEG.transformForType(destType, false);
                    if (transform != JPEG.ADOBE_IMPOSSIBLE) {
                        writeAdobe = true;
                        newAdobeTransform = transform;
                    }
                }
            }
        } else { // no destination type
            if (metadata == null) {
                if (fullImage) {  // no dest, no metadata, full image
                    // Use default metadata matching the image and param
                    metadata = new JPEGMetadata(new ImageTypeSpecifier(rimage),
                                                param, this);
                    inCsType = getSrcCSType(rimage);
                    outCsType = getDefaultDestCSType(rimage);
                }
                // else no dest, no metadata, not an image, 
                // so no special headers, no color conversion
            } else { // no dest type, but there is metadata
                checkSOFBands(sof, numBandsUsed);
                if (fullImage) {  // no dest, metadata, image
                    // Check that the metadata and the image match

                    ImageTypeSpecifier inputType = 
                        new ImageTypeSpecifier(rimage);

                    inCsType = getSrcCSType(rimage);

                    if (cm != null) {
                        boolean alpha = cm.hasAlpha();
                        ColorSpace cs = cm.getColorSpace();
                        switch (cs.getType()) {
                        case ColorSpace.TYPE_GRAY:
                            if (!alpha) {
                                outCsType = JPEG.JCS_GRAYSCALE;
                            } else {
                                if (jfif != null) {
                                    ignoreJFIF = true;
                                    warningOccurred
                                    (WARNING_IMAGE_METADATA_JFIF_MISMATCH);
                                }
                                // out colorspace remains unknown
                            }
                            if ((adobe != null) 
                                && (adobe.transform != JPEG.ADOBE_UNKNOWN)) {
                                newAdobeTransform = JPEG.ADOBE_UNKNOWN;
                                warningOccurred
                                (WARNING_IMAGE_METADATA_ADOBE_MISMATCH);
                            }
                            break;
                        case ColorSpace.TYPE_RGB:
                            if (!alpha) {
                                if (jfif != null) {
                                    outCsType = JPEG.JCS_YCbCr;
                                } else if (adobe != null) {
                                    switch (adobe.transform) { 
                                    case JPEG.ADOBE_UNKNOWN:
                                        outCsType = JPEG.JCS_RGB;
                                        break;
                                    case JPEG.ADOBE_YCC:
                                        outCsType = JPEG.JCS_YCbCr;
                                        break;
                                    default:
                                        warningOccurred
                                        (WARNING_IMAGE_METADATA_ADOBE_MISMATCH);
                                        newAdobeTransform = JPEG.ADOBE_UNKNOWN;
                                        outCsType = JPEG.JCS_RGB;
                                        break;
                                    }
                                } else {
                                    // consult the ids
                                    int outCS = sof.getIDencodedCSType();
                                    // if they don't resolve it,
                                    // consult the sampling factors
                                    if (outCS != JPEG.JCS_UNKNOWN) {
                                        outCsType = outCS;
                                    } else {
                                        boolean subsampled = 
                                        isSubsampled(sof.componentSpecs);
                                        if (subsampled) {
                                            outCsType = JPEG.JCS_YCbCr;
                                        } else {
                                            outCsType = JPEG.JCS_RGB;
                                        }
                                    }
                                }
                            } else { // RGBA
                                if (jfif != null) {
                                    ignoreJFIF = true;
                                    warningOccurred
                                    (WARNING_IMAGE_METADATA_JFIF_MISMATCH);
                                }
                                if (adobe != null) {
                                    if (adobe.transform 
                                        != JPEG.ADOBE_UNKNOWN) {
                                        newAdobeTransform = JPEG.ADOBE_UNKNOWN;
                                        warningOccurred
                                        (WARNING_IMAGE_METADATA_ADOBE_MISMATCH);
                                    }
                                    outCsType = JPEG.JCS_RGBA;
                                } else {
                                    // consult the ids
                                    int outCS = sof.getIDencodedCSType();
                                    // if they don't resolve it,
                                    // consult the sampling factors
                                    if (outCS != JPEG.JCS_UNKNOWN) {
                                        outCsType = outCS;
                                    } else {
                                        boolean subsampled = 
                                        isSubsampled(sof.componentSpecs);
                                        outCsType = subsampled ? 
                                            JPEG.JCS_YCbCrA : JPEG.JCS_RGBA;
                                    }
                                }
                            }
                            break;
                        case ColorSpace.TYPE_3CLR:
                            if (cs == JPEG.YCC) {
                                if (!alpha) {
                                    if (jfif != null) {
                                        convertTosRGB = true;
                                        convertOp = 
                                        new ColorConvertOp(cs, 
                                                           JPEG.sRGB, 
                                                           null);
                                        outCsType = JPEG.JCS_YCbCr;
                                    } else if (adobe != null) {
                                        if (adobe.transform 
                                            != JPEG.ADOBE_YCC) {
                                            newAdobeTransform = JPEG.ADOBE_YCC;
                                            warningOccurred
                                            (WARNING_IMAGE_METADATA_ADOBE_MISMATCH);
                                        }
                                        outCsType = JPEG.JCS_YCC;
                                    } else {
                                        outCsType = JPEG.JCS_YCC;
                                    }
                                } else { // PhotoYCCA
                                    if (jfif != null) {
                                        ignoreJFIF = true;
                                        warningOccurred
                                        (WARNING_IMAGE_METADATA_JFIF_MISMATCH);
                                    } else if (adobe != null) {
                                        if (adobe.transform 
                                            != JPEG.ADOBE_UNKNOWN) {
                                            newAdobeTransform 
                                            = JPEG.ADOBE_UNKNOWN;
                                            warningOccurred
                                            (WARNING_IMAGE_METADATA_ADOBE_MISMATCH);
                                        }
                                    }
                                    outCsType = JPEG.JCS_YCCA;
                                }
                            }
                        }
                    }
                } // else no dest, metadata, not an image.  Defaults ok
            }
        }

        boolean metadataProgressive = false;
        int [] scans = null;

        if (metadata != null) {
            if (sof == null) {
                sof = (SOFMarkerSegment) metadata.findMarkerSegment
                    (SOFMarkerSegment.class, true);
            }
            if ((sof != null) && (sof.tag == JPEG.SOF2)) {
                metadataProgressive = true;
                if (progressiveMode == ImageWriteParam.MODE_COPY_FROM_METADATA) {
                    scans = collectScans(metadata, sof);  // Might still be null
                } else {
                    numScans = 0;
                }
            }
        }

        // Set up a boolean to indicate whether we need to call back to 
        // write metadata
        boolean haveMetadata = 
            ((metadata != null) || writeDefaultJFIF || writeAdobe);

        // Now that we have dealt with metadata, finalize our tables set up

        // Are we going to write tables?  By default, yes.
        boolean writeDQT = true;
        boolean writeDHT = true;

        // But if the metadata has no tables, no.
        DQTMarkerSegment dqt = null;
        DHTMarkerSegment dht = null;

        int restartInterval = 0;

        if (metadata != null) {
            dqt = (DQTMarkerSegment) metadata.findMarkerSegment
                (DQTMarkerSegment.class, true);
            dht = (DHTMarkerSegment) metadata.findMarkerSegment
                (DHTMarkerSegment.class, true);
            DRIMarkerSegment dri = 
                (DRIMarkerSegment) metadata.findMarkerSegment
                (DRIMarkerSegment.class, true);
            if (dri != null) {
                restartInterval = dri.restartInterval;
            }

            if (dqt == null) {
                writeDQT = false;
            }
            if (dht == null) {
                writeDHT = false;  // Ignored if optimizeHuffman is true
            }
        }

        // Whether we write tables or not, we need to figure out which ones 
        // to use
        if (qTables == null) { // Get them from metadata, or use defaults
            if (dqt != null) {
                qTables = collectQTablesFromMetadata(metadata);
            } else if (streamQTables != null) { 
                qTables = streamQTables;
            } else if ((jparam != null) && (jparam.areTablesSet())) {
                qTables = jparam.getQTables();
            } else {
                qTables = JPEG.getDefaultQTables();
            }

        }

        // If we are optimizing, we don't want any tables.
        if (optimizeHuffman == false) {
            // If they were for progressive scans, we can't use them.
            if ((dht != null) && (metadataProgressive == false)) {
                DCHuffmanTables = collectHTablesFromMetadata(metadata, true);
                ACHuffmanTables = collectHTablesFromMetadata(metadata, false);
            } else if (streamDCHuffmanTables != null) { 
                DCHuffmanTables = streamDCHuffmanTables;
                ACHuffmanTables = streamACHuffmanTables;
            } else if ((jparam != null) && (jparam.areTablesSet())) {
                DCHuffmanTables = jparam.getDCHuffmanTables();
                ACHuffmanTables = jparam.getACHuffmanTables();
            } else {
                DCHuffmanTables = JPEG.getDefaultHuffmanTables(true);
                ACHuffmanTables = JPEG.getDefaultHuffmanTables(false);
            }
        }

        // By default, ids are 1 - N, no subsampling
        int [] componentIds = new int[numBandsUsed];
        int [] HsamplingFactors = new int[numBandsUsed];
        int [] VsamplingFactors = new int[numBandsUsed];
        int [] QtableSelectors = new int[numBandsUsed];
        for (int i = 0; i < numBandsUsed; i++) {
            componentIds[i] = i+1;
            HsamplingFactors[i] = 1;
            VsamplingFactors[i] = 1;
            QtableSelectors[i] = 0;
        }

        // Now override them with the contents of sof, if there is one
        if (sof != null) {
            for (int i = 0; i < numBandsUsed; i++) {
                componentIds[i] = sof.componentSpecs[i].componentId;
                HsamplingFactors[i] = sof.componentSpecs[i].HsamplingFactor;
                VsamplingFactors[i] = sof.componentSpecs[i].VsamplingFactor;
                QtableSelectors[i] = sof.componentSpecs[i].QtableSelector;
            }
        }

        sourceXOffset += gridX;
        sourceWidth -= gridX;
        sourceYOffset += gridY;
        sourceHeight -= gridY;

        int destWidth = (sourceWidth + periodX - 1)/periodX;
        int destHeight = (sourceHeight + periodY - 1)/periodY;

        // Create an appropriate 1-line databuffer for writing
        int lineSize = sourceWidth*numBandsUsed;
        
        DataBufferByte buffer = new DataBufferByte(lineSize);

        // Create a raster from that
        int [] bandOffs = JPEG.bandOffsets[numBandsUsed-1];
        
        raster = Raster.createInterleavedRaster(buffer, 
                                                sourceWidth, 1, 
                                                lineSize,
                                                numBandsUsed,
                                                bandOffs,
                                                null);

        // Call the writer, who will call back for every scanline
        // Check for progess, update listeners (don't call back if none)
        boolean callbackProgress = progressListeners != null;

        processImageStarted(currentImage);
            
        boolean aborted = false;

        if (debug) {
            System.out.println("inCsType: " + inCsType);
            System.out.println("outCsType: " + outCsType);
        }

        // Synchronize to protect the native linked list
        synchronized (JPEGImageReader.class) {
            aborted = writeImage(buffer.getData(), 
                                 inCsType, outCsType,
                                 numBandsUsed,
                                 bandSizes,
                                 sourceWidth,
                                 destWidth, destHeight,
                                 periodX, periodY,
                                 qTables,
                                 writeDQT,
                                 DCHuffmanTables,
                                 ACHuffmanTables,
                                 writeDHT,
                                 optimizeHuffman,
                                 (progressiveMode 
                                  != ImageWriteParam.MODE_DISABLED),
                                 numScans,
                                 scans,
                                 componentIds,
                                 HsamplingFactors,
                                 VsamplingFactors,
                                 QtableSelectors,
                                 haveMetadata,
                                 restartInterval,
                                 callbackProgress);
        }

        if (aborted) {
            processWriteAborted();
        } else {
            processImageComplete();
        }

        ios.flush();
        currentImage++;  // After a successful write
    }

    public void prepareWriteSequence(IIOMetadata streamMetadata) 
        throws IOException {
        if (ios == null) {
            throw new IllegalStateException("Output has not been set!");
        }

        /*
         * from jpeg_metadata.html:
         * If no stream metadata is supplied to
         * <code>ImageWriter.prepareWriteSequence</code>, then no 
         * tables-only image is written.  If stream metadata containing
         * no tables is supplied to
         * <code>ImageWriter.prepareWriteSequence</code>, then a tables-only
         * image containing default visually lossless tables is written.
         */
        if (streamMetadata != null) {
            if (streamMetadata instanceof JPEGMetadata) {
                // write a complete tables-only image at the beginning of 
                // the stream.
                JPEGMetadata jmeta = (JPEGMetadata) streamMetadata;
                if (jmeta.isStream == false) {
                    throw new IllegalArgumentException
                        ("Invalid stream metadata object.");
                }
                // Check that we are
                // at the beginning of the stream, or can go there, and haven't
                // written out the metadata already.
                if (currentImage != 0) {
                    throw new IIOException
                        ("JPEG Stream metadata must precede all images");
                }
                if (sequencePrepared == true) {
                    throw new IIOException("Stream metadata already written!");
                }
                
                // Set the tables
                // If the metadata has no tables, use default tables.
                streamQTables = collectQTablesFromMetadata(jmeta);
                if (debug) {
                    System.out.println("after collecting from stream metadata, "
                                       + "streamQTables.length is " 
                                       + streamQTables.length);
                }
                if (streamQTables == null) {
                    streamQTables = JPEG.getDefaultQTables();
                }
                streamDCHuffmanTables = 
                    collectHTablesFromMetadata(jmeta, true);
                if (streamDCHuffmanTables == null) {
                    streamDCHuffmanTables = JPEG.getDefaultHuffmanTables(true);
                }
                streamACHuffmanTables = 
                    collectHTablesFromMetadata(jmeta, false);
                if (streamACHuffmanTables == null) {
                    streamACHuffmanTables = JPEG.getDefaultHuffmanTables(false);
                }

                // Now write them out
                writeTables(streamQTables, 
                            streamDCHuffmanTables, 
                            streamACHuffmanTables);
            } else { 
                throw new IIOException("Stream metadata must be JPEG metadata");
            }
        }
        sequencePrepared = true;
    }

    public void writeToSequence(IIOImage image, ImageWriteParam param)
        throws IOException {
        if (sequencePrepared == false) {
            throw new IllegalStateException("sequencePrepared not called!");
        }
        // In the case of JPEG this does nothing different from write
        write(null, image, param);
    }

    public void endWriteSequence() throws IOException {
        if (sequencePrepared == false) {
            throw new IllegalStateException("sequencePrepared not called!");
        }
        sequencePrepared = false;
    }

    public synchronized void abort() {
        super.abort();
        // Synchronize to protect the native linked list
        synchronized (JPEGImageReader.class) {
            abortWrite();
        }
    }

    public void reset() {
        // reset C structures
        // Synchronize to protect the native linked list
        synchronized (JPEGImageReader.class) {
            resetWriter();
        }
        // reset local Java structures
        super.reset();
        ios = null;
        srcRas = null;
        raster = null;
        convertTosRGB = false;
        currentImage = 0;
        numScans = 0;
        System.gc();
    }

    public void dispose() {
        // Synchronize to protect the native linked list
        synchronized (JPEGImageReader.class) {
            disposeWriter();
        }
    }
    
    public void finalize() {
        dispose();
    }

    ////////// End of public API

    ///////// Package-access API

    /**
     * Called by the native code and the metadata class to signal a 
     * warning.  The argument must be a valid index into the warning
     * strings array above, or a fatal error is thrown.  The code is
     * used to lookup the message to be used when sending warnings to
     * listeners.  All localization must be done here in Java rather
     * than in the native code.
     * FIXME  The current implementation is not localized at all.
     */
    void warningOccurred(int code) {
        if ((code < 0) || (code >= warnings.length)){
            throw new InternalError("Invalid warning index");
        }
        processWarningOccurred(currentImage, warnings[code]);
    }

    /**
     * The library has it's own error facility that emits warning messages.
     * This routine is called by the native code when it has already 
     * formatted a string for output.  
     * XXX  For proper localization of all warning messages,
     * the sun_jpeg_output_message routine in the native code should
     * send only the codes and parameters to warningOccurred, above,
     * which will then format and send the warnings, using localized
     * strings.  For now, this prevents library warnings from being
     * printed to stderr.
     */
    protected void warningWithMessage(String msg) {
        processWarningOccurred(currentImage, msg);
    }

    ///////// End of Package-access API

    ///////// Private methods

    ///////// Metadata handling

    private void checkSOFBands(SOFMarkerSegment sof, int numBandsUsed) 
        throws IIOException {
        // Does the metadata frame header, if any, match numBandsUsed?
        if (sof != null) {
            if (sof.componentSpecs.length != numBandsUsed) {
                throw new IIOException
                    ("Metadata components != number of destination bands");
            }
        }
    }

    private void checkJFIF(JFIFMarkerSegment jfif,
                           ImageTypeSpecifier type,
                           boolean input) {
        if (jfif != null) {
            if (!JPEG.isJFIFcompliant(type, input)) {
                ignoreJFIF = true;  // type overrides metadata
                warningOccurred(input 
                                ? WARNING_IMAGE_METADATA_JFIF_MISMATCH
                                : WARNING_DEST_METADATA_JFIF_MISMATCH);
            }
        }
    }

    private void checkAdobe(AdobeMarkerSegment adobe,
                           ImageTypeSpecifier type,
                           boolean input) {
        if (adobe != null) {
            int rightTransform = JPEG.transformForType(type, input);
            if (adobe.transform != rightTransform) {
                warningOccurred(input 
                                ? WARNING_IMAGE_METADATA_ADOBE_MISMATCH
                                : WARNING_DEST_METADATA_ADOBE_MISMATCH);
                if (rightTransform == JPEG.ADOBE_IMPOSSIBLE) {
                    ignoreAdobe = true;
                } else {
                    newAdobeTransform = rightTransform;
                }
            }
        }
    }

    /**
     * Collect all the scan info from the given metadata, and 
     * organize it into the scan info array required by the
     * IJG libray.  It is much simpler to parse out this
     * data in Java and then just copy the data in C.
     */
    private int [] collectScans(JPEGMetadata metadata, 
                                SOFMarkerSegment sof) {
        List segments = new ArrayList();
        int SCAN_SIZE = 9;
        int MAX_COMPS_PER_SCAN = 4;
        for (Iterator iter = metadata.markerSequence.iterator();
             iter.hasNext();) {
            MarkerSegment seg = (MarkerSegment) iter.next();
            if (seg instanceof SOSMarkerSegment) {
                segments.add(seg);
            }
        }
        int [] retval = null;
        numScans = 0;
        if (!segments.isEmpty()) {
            numScans = segments.size();
            retval = new int [numScans*SCAN_SIZE];
            int index = 0;
            for (int i = 0; i < numScans; i++) {
                SOSMarkerSegment sos = (SOSMarkerSegment) segments.get(i);
                retval[index++] = sos.componentSpecs.length; // num comps
                for (int j = 0; j < MAX_COMPS_PER_SCAN; j++) {
                    if (j < sos.componentSpecs.length) {
                        int compSel = sos.componentSpecs[j].componentSelector;
                        for (int k = 0; k < sof.componentSpecs.length; k++) {
                            if (compSel == sof.componentSpecs[k].componentId) {
                                retval[index++] = k;
                                break; // out of for over sof comps
                            }
                        }
                    } else {
                        retval[index++] = 0;
                    }
                }
                retval[index++] = sos.startSpectralSelection;
                retval[index++] = sos.endSpectralSelection;
                retval[index++] = sos.approxHigh;
                retval[index++] = sos.approxLow;
            }
        }
        return retval;
    }

    /**
     * Finds all DQT marker segments and returns all the q 
     * tables as a single array of JPEGQTables.
     */
    private JPEGQTable [] collectQTablesFromMetadata
        (JPEGMetadata metadata) {
        ArrayList tables = new ArrayList();
        Iterator iter = metadata.markerSequence.iterator();
        while (iter.hasNext()) {
            MarkerSegment seg = (MarkerSegment) iter.next();
            if (seg instanceof DQTMarkerSegment) {
                DQTMarkerSegment dqt = 
                    (DQTMarkerSegment) seg;
                tables.addAll(dqt.tables);
            }
        }
        JPEGQTable [] retval = null;
        if (tables.size() != 0) {
            retval = new JPEGQTable[tables.size()];
            for (int i = 0; i < retval.length; i++) {
                retval[i] = 
                    new JPEGQTable(((DQTMarkerSegment.Qtable)tables.get(i)).data);
            }
        }
        return retval;
    }

    /**
     * Finds all DHT marker segments and returns all the q 
     * tables as a single array of JPEGQTables.  The metadata
     * must not be for a progressive image, or an exception
     * will be thrown when two Huffman tables with the same
     * table id are encountered.
     */
    private JPEGHuffmanTable[] collectHTablesFromMetadata
        (JPEGMetadata metadata, boolean wantDC) throws IIOException {
        ArrayList tables = new ArrayList();
        Iterator iter = metadata.markerSequence.iterator();
        while (iter.hasNext()) {
            MarkerSegment seg = (MarkerSegment) iter.next();
            if (seg instanceof DHTMarkerSegment) {
                DHTMarkerSegment dht = 
                    (DHTMarkerSegment) seg;
                for (int i = 0; i < dht.tables.size(); i++) {
                    DHTMarkerSegment.Htable htable = 
                        (DHTMarkerSegment.Htable) dht.tables.get(i);
                    if (htable.tableClass == (wantDC ? 0 : 1)) {
                        tables.add(htable);
                    }
                }
            }
        }
        JPEGHuffmanTable [] retval = null;
        if (tables.size() != 0) {
            DHTMarkerSegment.Htable [] htables = 
                new DHTMarkerSegment.Htable[tables.size()];
            tables.toArray(htables);
            retval = new JPEGHuffmanTable[tables.size()];
            for (int i = 0; i < retval.length; i++) {
                retval[i] = null;
                for (int j = 0; j < tables.size(); j++) {
                    if (htables[j].tableID == i) {
                        if (retval[i] != null) {
                            throw new IIOException("Metadata has duplicate Htables!");
                        }
                        retval[i] = new JPEGHuffmanTable(htables[j].numCodes, 
                                                         htables[j].values);
                    }
                }
            }
        }
        
        return retval;
    }

    /////////// End of metadata handling

    ////////////// ColorSpace conversion
    
    private int getSrcCSType(RenderedImage rimage) {
        int retval = JPEG.JCS_UNKNOWN;
        ColorModel cm = rimage.getColorModel();
        if (cm != null) {
            boolean alpha = cm.hasAlpha();
            ColorSpace cs = cm.getColorSpace();
            switch (cs.getType()) {
            case ColorSpace.TYPE_GRAY:
                retval = JPEG.JCS_GRAYSCALE;
                break;
            case ColorSpace.TYPE_RGB:
                if (alpha) {
                    retval = JPEG.JCS_RGBA;
                } else {
                    retval = JPEG.JCS_RGB;
                }
                break;
            case ColorSpace.TYPE_YCbCr:
                if (alpha) {
                    retval = JPEG.JCS_YCbCrA;
                } else {
                    retval = JPEG.JCS_YCbCr;
                }
                break;
            case ColorSpace.TYPE_3CLR:
                if (cs == JPEG.YCC) {
                    if (alpha) {
                        retval = JPEG.JCS_YCCA;
                    } else {
                        retval = JPEG.JCS_YCC;
                    }
                }
            case ColorSpace.TYPE_CMYK:
                retval = JPEG.JCS_CMYK;
                break;
            }
        }
        return retval;
    }

    private int getDestCSType(ImageTypeSpecifier destType) {
        ColorModel cm = destType.getColorModel();
        boolean alpha = cm.hasAlpha();
        ColorSpace cs = cm.getColorSpace();
        int retval = JPEG.JCS_UNKNOWN;
        switch (cs.getType()) {
        case ColorSpace.TYPE_GRAY:
                retval = JPEG.JCS_GRAYSCALE;
                break;
            case ColorSpace.TYPE_RGB:
                if (alpha) {
                    retval = JPEG.JCS_RGBA;
                } else {
                    retval = JPEG.JCS_RGB;
                }
                break;
            case ColorSpace.TYPE_YCbCr:
                if (alpha) {
                    retval = JPEG.JCS_YCbCrA;
                } else {
                    retval = JPEG.JCS_YCbCr;
                }
                break;
            case ColorSpace.TYPE_3CLR:
                if (cs == JPEG.YCC) {
                    if (alpha) {
                        retval = JPEG.JCS_YCCA;
                    } else {
                        retval = JPEG.JCS_YCC;
                    }
                }
            case ColorSpace.TYPE_CMYK:
                retval = JPEG.JCS_CMYK;
                break;
            }
        return retval;
        }

    private int getDefaultDestCSType(RenderedImage rimage) {
        int retval = JPEG.JCS_UNKNOWN;
        ColorModel cm = rimage.getColorModel();
        if (cm != null) {
            boolean alpha = cm.hasAlpha();
            ColorSpace cs = cm.getColorSpace();
            switch (cs.getType()) {
            case ColorSpace.TYPE_GRAY:
                retval = JPEG.JCS_GRAYSCALE;
                break;
            case ColorSpace.TYPE_RGB:
                if (alpha) {
                    retval = JPEG.JCS_YCbCrA;
                } else {
                    retval = JPEG.JCS_YCbCr;
                }
                break;
            case ColorSpace.TYPE_YCbCr:
                if (alpha) {
                    retval = JPEG.JCS_YCbCrA;
                } else {
                    retval = JPEG.JCS_YCbCr;
                }
                break;
            case ColorSpace.TYPE_3CLR:
                if (cs == JPEG.YCC) {
                    if (alpha) {
                        retval = JPEG.JCS_YCCA;
                    } else {
                        retval = JPEG.JCS_YCC;
                    }
                }
            case ColorSpace.TYPE_CMYK:
                retval = JPEG.JCS_YCCK;
                break;
            }
        }
        return retval;
    }

    private boolean isSubsampled(SOFMarkerSegment.ComponentSpec [] specs) {
        int hsamp0 = specs[0].HsamplingFactor;
        int vsamp0 = specs[0].VsamplingFactor;
        for (int i = 1; i < specs.length; i++) {
            if ((specs[i].HsamplingFactor != hsamp0) ||
                (specs[i].HsamplingFactor != hsamp0))
                return true;
        }
        return false;
    }

    ////////////// End of ColorSpace conversion

    ////////////// Native methods and callbacks

    /** Sets up static native structures. */
    private static native void initWriterIDs(Class iosClass, 
                                             Class qTableClass,
                                             Class huffClass);

    /** Sets up per-writer native structures. */
    private native void initJPEGImageWriter();

    /** Sets up native structures for output stream */
    private native void setDest(ImageOutputStream ios);

    /**
     * Returns <code>true</code> if the write was aborted.
     */
    private native boolean writeImage(byte [] data,
                                      int inCsType, int outCsType,
                                      int numBands,
                                      int [] bandSizes,
                                      int srcWidth,
                                      int destWidth, int destHeight,
                                      int stepX, int stepY,
                                      JPEGQTable [] qtables,
                                      boolean writeDQT,
                                      JPEGHuffmanTable[] DCHuffmanTables,
                                      JPEGHuffmanTable[] ACHuffmanTables,
                                      boolean writeDHT,
                                      boolean optimizeHuffman,
                                      boolean progressive,
                                      int numScans,
                                      int [] scans,
                                      int [] componentIds,
                                      int [] HsamplingFactors,
                                      int [] VsamplingFactors,
                                      int [] QtableSelectors,
                                      boolean haveMetadata,
                                      int restartInterval,
                                      boolean progressWanted);


    /**
     * Writes the metadata out when called by the native code, 
     * which will have already written the header to the stream
     * and established the library state.  This is simpler than
     * breaking the write call in two.
     * FIXME - thumbnails and icc profiles will need to be dealt with here
     */
    private void writeMetadata() throws IOException {
        if (metadata == null) {
            if (writeDefaultJFIF) {
                JFIFMarkerSegment.writeDefaultJFIF(ios);
            }
            if (writeAdobe) {
                AdobeMarkerSegment.writeAdobeSegment(ios, newAdobeTransform);
            }
        } else {
            metadata.writeToStream(ios, 
                                   ignoreJFIF, 
                                   ignoreAdobe, 
                                   newAdobeTransform, 
                                   this);
        }
    }

    /**
     * Write out a tables-only image to the stream.
     */
    private native void writeTables(JPEGQTable [] qtables,
                                    JPEGHuffmanTable[] DCHuffmanTables,
                                    JPEGHuffmanTable[] ACHuffmanTables);

    /**
     * Put the scanline y of the source ROI view Raster into the
     * 1-line Raster for writing.  This handles ROI and band
     * rearrangements, and expands indexed images.  Subsampling is
     * done in the native code.
     * This is called by the native code.
     */
    private void grabPixels(int y) {

        Raster sourceLine = null;
        if (indexed) {
            sourceLine = srcRas.createChild(sourceXOffset, 
                                            sourceYOffset+y,
                                            sourceWidth, 1,
                                            0, 0,
                                            new int [] {0});
            BufferedImage temp = indexCM.convertToIntDiscrete(sourceLine,
                                                              false);
            sourceLine = temp.getRaster();
        } else {
            sourceLine = srcRas.createChild(sourceXOffset, 
                                            sourceYOffset+y,
                                            sourceWidth, 1,
                                            0, 0,
                                            srcBands);

        }
        if (debug) {
            System.out.print("First 5 pixels of sourceLine:");
            for (int i = 0; i < 5; i++){
                SampleModel sm = sourceLine.getSampleModel();
                DataBuffer buf = sourceLine.getDataBuffer();
                int [] pixel = sm.getPixel(i, 0, (int[])null, buf);
                System.out.print(" " + pixel[0]);
                System.out.print("," + pixel[1]);
                System.out.print("," + pixel[2] + " ");
            }
            System.out.println();
        }
        if (convertTosRGB) {
            // The first time through, converted is null, so
            // a new raster is allocated.  It is then reused
            // on subsequent lines.
            converted = convertOp.filter(sourceLine, converted);
            sourceLine = converted;
        }
        raster.setRect(0, 0, sourceLine);
        if (debug) {
            System.out.print("First 5 pixels of raster after setRect:");
            for (int i = 0; i < 5; i++){
                SampleModel sm = raster.getSampleModel();
                DataBuffer buf = raster.getDataBuffer();
                int [] pixel = sm.getPixel(i, 0, (int[]) null, buf);
                System.out.print(" " + pixel[0]);
                System.out.print("," + pixel[1]);
                System.out.print("," + pixel[2] + " ");
            }
            System.out.println();
        }
        if ((y > 0) &&(y%8 == 0)) {  // Every 8 scanlines
            processImageProgress((float) y / (float) sourceHeight * 100.0F);
        }
    }

    /** Aborts the current write in the native code */
    private native void abortWrite();

    /** Resets native structures */
    private native void resetWriter();

    /** Releases native structures */
    private native void disposeWriter();

}
