/*
 * @(#)JFIFMarkerSegment.java	1.2 01/03/21
 *
 * 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.metadata.IIOInvalidTreeException;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.ImageOutputStream;

import java.awt.color.ICC_Profile;
import java.awt.color.ICC_ColorSpace;
import java.io.IOException;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;

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

/**
 * A JFIF (JPEG File Interchange Format) APP0 (Application-Specific)
 * marker segment.
 */
class JFIFMarkerSegment extends MarkerSegment {
    int majorVersion;
    int minorVersion;
    int resUnits;
    int Xdensity;
    int Ydensity;
    int thumbWidth;
    int thumbHeight;
    JFIFExtensionMarkerSegment extSegment = null; // optional JFXX
    ICCMarkerSegment iccSegment = null; // optional ICC
    private static final int DATA_SIZE = 14;
    private static final int ID_SIZE = 5;
    /**
     * Set to <code>true</code> when reading the chunks of an
     * ICC profile.  All chunks are consolidated to create a single
     * "segment" containing all the chunks.  This flag is a state
     * variable identifying whether to construct a new segment or
     * append to an old one.
     */
    private boolean inICC = false;

    /**
     * A placeholder for an ICC profile marker segment under
     * construction.  The segment is not added to the list
     * until all chunks have been read.
     */
    private ICCMarkerSegment tempICCSegment = null;


    JFIFMarkerSegment() {
        super(JPEG.APP0);
        majorVersion = 1;
        minorVersion = 2;
        resUnits = JPEG.DENSITY_UNIT_ASPECT_RATIO;
        Xdensity = 1;
        Ydensity = 1;
        thumbWidth = 0;
        thumbHeight = 0;
    }

    JFIFMarkerSegment(JPEGBuffer buffer) throws IOException {
        super(buffer);
        buffer.bufPtr += ID_SIZE;  // skip the id, we already checked it
            
        majorVersion = buffer.buf[buffer.bufPtr++];
        minorVersion = buffer.buf[buffer.bufPtr++];
        resUnits = buffer.buf[buffer.bufPtr++];
        Xdensity = (buffer.buf[buffer.bufPtr++] & 0xff) << 8;
        Xdensity |= buffer.buf[buffer.bufPtr++] & 0xff;
        Ydensity = (buffer.buf[buffer.bufPtr++] & 0xff) << 8;
        Ydensity |= buffer.buf[buffer.bufPtr++] & 0xff;
        thumbWidth = buffer.buf[buffer.bufPtr++] & 0xff;
        thumbHeight = buffer.buf[buffer.bufPtr++] & 0xff;
        buffer.bufAvail -= DATA_SIZE;
    }

    JFIFMarkerSegment(Node node) throws IIOInvalidTreeException {
        this();
        updateFromNativeNode(node, true);
    }

    protected Object clone() {
        JFIFMarkerSegment newGuy = (JFIFMarkerSegment) super.clone();
        if (extSegment != null) {
            newGuy.extSegment = 
                (JFIFExtensionMarkerSegment) extSegment.clone();
        }
        if (iccSegment != null) {
            newGuy.iccSegment = (ICCMarkerSegment) iccSegment.clone();
        }
        return newGuy;
    }

    void addJFXX(JPEGBuffer buffer, JPEGImageReader reader)
        throws IOException {
        if (extSegment != null) {
            throw new IIOException
                ("> 1 JFXX Marker Segment not supported");
        }
        extSegment = new JFIFExtensionMarkerSegment(buffer, reader);
    }

    void addICC(JPEGBuffer buffer) throws IOException {
        if (inICC == false) {
            if (iccSegment != null) {
                throw new IIOException
                    ("> 1 ICC APP2 Marker Segment not supported");
            }
            tempICCSegment = new ICCMarkerSegment(buffer);
            if (inICC == false) { // Just one chunk
                iccSegment = tempICCSegment;
                tempICCSegment = null;
            }                
        } else {
            if (tempICCSegment.addData(buffer) == true) {
                iccSegment = tempICCSegment;
                tempICCSegment = null;
            }
        }
    }

    void addICC(ICC_ColorSpace cs) throws IOException {
        if (iccSegment != null) {
            throw new IIOException
                ("> 1 ICC APP2 Marker Segment not supported");
        }
        iccSegment = new ICCMarkerSegment(cs);
    }

    IIOMetadataNode getNativeNode() {
        IIOMetadataNode node = new IIOMetadataNode("app0JFIF");
        node.setAttribute("majorVersion", Integer.toString(majorVersion));
        node.setAttribute("minorVersion", Integer.toString(minorVersion));
        node.setAttribute("resUnits", Integer.toString(resUnits));
        node.setAttribute("Xdensity", Integer.toString(Xdensity));
        node.setAttribute("Ydensity", Integer.toString(Ydensity));
        node.setAttribute("thumbWidth", Integer.toString(thumbWidth));
        node.setAttribute("thumbHeight", Integer.toString(thumbHeight));
        if (extSegment != null) {
            node.appendChild(extSegment.getNativeNode());
        }
        if (iccSegment != null) {
            node.appendChild(iccSegment.getNativeNode());
        }

        return node;
    }

    void updateFromNativeNode(Node node, boolean fromScratch) 
        throws IIOInvalidTreeException {
        // none of the attributes are required
        NamedNodeMap attrs = node.getAttributes();
        if (attrs.getLength() > 0) {
            int value = getAttributeValue(node, attrs, "majorVersion", 
                                          0, 255, false);
            majorVersion = (value != -1) ? value : majorVersion;
            value = getAttributeValue(node, attrs, "minorVersion", 
                                      0, 255, false);
            minorVersion = (value != -1) ? value : minorVersion;
            value = getAttributeValue(node, attrs, "resUnits", 0, 2, false);
            resUnits = (value != -1) ? value : resUnits;
            value = getAttributeValue(node, attrs, "Xdensity", 1, 65535, false);
            Xdensity = (value != -1) ? value : Xdensity;
            value = getAttributeValue(node, attrs, "Ydensity", 1, 65535, false);
            Ydensity = (value != -1) ? value : Ydensity;
            value = getAttributeValue(node, attrs, "thumbWidth", 0, 255, false);
            thumbWidth = (value != -1) ? value : thumbWidth;
            value = getAttributeValue(node, attrs, "thumbHeight", 0, 255, false);
            thumbHeight = (value != -1) ? value : thumbHeight;
        }
        if (node.hasChildNodes()) {
            NodeList children = node.getChildNodes();
            int count = children.getLength();
            if (count > 2) {
                throw new IIOInvalidTreeException
                    ("app0JFIF node cannot have > 2 children", node);
            }
            for (int i = 0; i < count; i++) {
                Node child = children.item(i);
                String name = child.getNodeName();
                if (name.equals("app0JFXX")) {
                    if ((extSegment != null) && fromScratch) {
                        throw new IIOInvalidTreeException
                            ("> 1 JFXX Marker Segment not supported", node);
                    }
                    extSegment = new JFIFExtensionMarkerSegment(child);
                }
                if (name.equals("app2ICC")) {
                    if ((iccSegment != null) && fromScratch) {
                        throw new IIOInvalidTreeException
                            ("> 1 ICC APP2 Marker Segment not supported", node);
                    }
                    iccSegment = new ICCMarkerSegment(child);
                }
            }
        }
    }

    /**
     * Writes the data for this segment to the stream in
     * valid JPEG format.
     */
    void write(ImageOutputStream ios) throws IOException {
        length = 16;
        writeTag(ios);
        byte [] id = {0x4A, 0x46, 0x49, 0x46, 0x00};
        ios.write(id);
        ios.write(majorVersion);
        ios.write(minorVersion);
        ios.write(resUnits);
        write2bytes(ios, Xdensity);
        write2bytes(ios, Ydensity);
        // No thumbnail
        ios.write(0);
        ios.write(0);
    }

    /**
     * Write out a default JFIF marker segment to the given
     * output stream.
     */
    static void writeDefaultJFIF(ImageOutputStream ios) 
        throws IOException {
        (new JFIFMarkerSegment()).write(ios);
    }

    void print() {
        printTag("JFIF");
        System.out.print("Version ");
        System.out.print(majorVersion);
        System.out.println(".0"
                           + Integer.toString(minorVersion));
        System.out.print("Resolution units: ");
        System.out.println(resUnits);
        System.out.print("X density: ");
        System.out.println(Xdensity);
        System.out.print("Y density: ");
        System.out.println(Ydensity);
        System.out.print("Thumbnail Width: ");
        System.out.println(thumbWidth);
        System.out.print("Thumbnail Height: ");
        System.out.println(thumbHeight);
        if (extSegment != null) {
            extSegment.print();
        }
        if (iccSegment != null) {
            iccSegment.print();
        }
    }

    /**
     * A JFIF extension APP0 marker segment.
     */
    class JFIFExtensionMarkerSegment extends MarkerSegment {
        int code;
        private static final int THUMB_JPEG = 0x10;
        private static final int THUMB_PALETTE = 0x11;
        private static final int THUMB_RGB = 0x13;
        JFIFThumb thumb;
        private static final int DATA_SIZE = 6;
        private static final int ID_SIZE = 5;

        JFIFExtensionMarkerSegment(JPEGBuffer buffer, JPEGImageReader reader) 
            throws IOException {
            
            super(buffer);
            buffer.bufPtr += ID_SIZE;  // skip the id, we already checked it

            code = buffer.buf[buffer.bufPtr++] & 0xff;
            buffer.bufAvail -= DATA_SIZE;
            if (code == THUMB_JPEG) {
                thumb = new JFIFThumbJPEG(buffer, length, reader);
            } else {
                buffer.loadBuf(2);
                int thumbX = buffer.buf[buffer.bufPtr++] & 0xff;
                int thumbY = buffer.buf[buffer.bufPtr++] & 0xff;
                buffer.bufAvail -= 2;
                // following constructors handle bufAvail
                if (code == THUMB_PALETTE) {
                    thumb = new JFIFThumbPalette(buffer, thumbX, thumbY);
                } else {
                    thumb = new JFIFThumbRGB(buffer, thumbX, thumbY);
                }
            }
        }

        JFIFExtensionMarkerSegment(Node node) throws IIOInvalidTreeException {
            super(JPEG.APP0);
            // there should be a single "extensionCode" attribute
            NamedNodeMap attrs = node.getAttributes();
            if (attrs.getLength() != 1) {
                throw new IIOInvalidTreeException
                    ("app0JFXX node must have exactly 1 attribute", node);
            }
            code = getAttributeValue(node, attrs, "extensionCode", 16, 19, true);
            if (code == 18) {  // Weird one in the middle of the range
                throw new IIOInvalidTreeException
                    ("invalid extensionCode attribute value", node);
            }
            // Now the child
            if (node.getChildNodes().getLength() != 1) {
                throw new IIOInvalidTreeException
                    ("app0JFXX node must have exactly 1 child", node);
            }
            Node child = node.getFirstChild();
            String name = child.getNodeName();
            if (name.equals("JFIFthumbJPEG")) {
                thumb = new JFIFThumbJPEG(child);
            } else if (name.equals("JFIFthumbPalette")) {
                thumb = new JFIFThumbPalette(child);
            } else if (name.equals("JFIFthumbRGB")) {
                thumb = new JFIFThumbRGB(child);
            } else {
                throw new IIOInvalidTreeException
                    ("unrecognized app0JFXX child node", node);
            }
        }

        protected Object clone() {
            JFIFExtensionMarkerSegment newGuy = 
                (JFIFExtensionMarkerSegment) super.clone();
            if (thumb != null) {
                newGuy.thumb = (JFIFThumb) thumb.clone();
            }
            return newGuy;
        }

        IIOMetadataNode getNativeNode() {
            IIOMetadataNode node = new IIOMetadataNode("app0JFXX");
            node.setAttribute("extensionCode", Integer.toString(code));
            node.appendChild(thumb.getNativeNode());
            return node;
        }

        void print() {
            printTag("JFXX");
            thumb.print();
        }
    }

    /**
     * A superclass for the varieties of thumbnails that can
     * be stored in a JFIF extension marker segment.
     */
    abstract class JFIFThumb implements Cloneable {
        abstract void print();

        abstract IIOMetadataNode getNativeNode();

        protected Object clone() {
            try {
                return super.clone();
            } catch (CloneNotSupportedException e) {} // won't happen
            return null;
        }

    }

    abstract class JFIFThumbUncompressed extends JFIFThumb {
        int thumbWidth;
        int thumbHeight;
        String name;

        JFIFThumbUncompressed(JPEGBuffer buffer, 
                              int width, 
                              int height, 
                              int skip, 
                              String name)
            throws IOException {

            thumbWidth = width;
            thumbHeight = height;
            // Now skip the thumbnail data
            buffer.skipData(skip);
            this.name = name;
        }

        JFIFThumbUncompressed(Node node, String name) 
            throws IIOInvalidTreeException {

            thumbWidth = 0;
            thumbHeight = 0;
            this.name = name;
            NamedNodeMap attrs = node.getAttributes();
            int count = attrs.getLength();
            if (count > 2) {
                throw new IIOInvalidTreeException
                    (name +" node cannot have > 2 attributes", node);
            }
            if (count != 0) {
                int value = getAttributeValue(node, attrs, "thumbWidth", 
                                              0, 255, false);
                thumbWidth = (value != -1) ? value : thumbWidth;
                value = getAttributeValue(node, attrs, "thumbHeight", 
                                          0, 255, false);
                thumbHeight = (value != -1) ? value : thumbHeight;
            }
        }

        IIOMetadataNode getNativeNode() {
            IIOMetadataNode node = new IIOMetadataNode(name);
            node.setAttribute("thumbWidth", Integer.toString(thumbWidth));
            node.setAttribute("thumbHeight", Integer.toString(thumbHeight));
            return node;
        }

        void print() {
            System.out.print(name + " width: ");
            System.out.println(thumbWidth);
            System.out.print(name + " height: ");
            System.out.println(thumbHeight);
        }

    }

    /**
     * A JFIF thumbnail stored as RGB, one byte per channel,
     * interleaved.
     */
    class JFIFThumbRGB extends JFIFThumbUncompressed {

        JFIFThumbRGB(JPEGBuffer buffer, int width, int height) throws IOException {
            super(buffer, width, height, width*height*3, "JFIFthumbRGB");
        }

        JFIFThumbRGB(Node node) throws IIOInvalidTreeException {
            super(node, "JFIFthumbRGB");
        }

    }

    /**
     * A JFIF thumbnail stored as an indexed palette image
     * using an RGB palette.
     */
    class JFIFThumbPalette extends JFIFThumbUncompressed {
        private static final int PALETTE_SIZE = 768;

        JFIFThumbPalette(JPEGBuffer buffer, int width, int height)
            throws IOException {
            super(buffer, 
                  width, 
                  height, 
                  PALETTE_SIZE + width * height,
                  "JFIFThumbPalette");
        }

        JFIFThumbPalette(Node node) throws IIOInvalidTreeException {
            super(node, "JFIFThumbPalette");
        }

    }


    /**
     * A JFIF thumbnail stored as a JPEG stream.  No JFIF or
     * JFIF extension markers are permitted.
     */
    class JFIFThumbJPEG extends JFIFThumb {
        JPEGMetadata thumbMetadata = null;
        private static final int PREAMBLE_SIZE = 6;
        
        JFIFThumbJPEG(JPEGBuffer buffer, 
                      int length, 
                      JPEGImageReader reader) throws IOException {

            long cur = buffer.iis.getStreamPosition();
            // Capture the stream position where the thumb starts
            long thumbStart = cur - buffer.bufAvail; 
            // Compute the final stream position
            long finalPos = thumbStart + (length - PREAMBLE_SIZE);
            // Set the stream back to the start of the thumbnail
            // and read its metadata
            buffer.iis.seek(thumbStart);
            thumbMetadata = new JPEGMetadata(false, true, buffer.iis, reader);
            // Set the stream to the computed final position
            buffer.iis.seek(finalPos);
            // Clear the now invalid buffer
            buffer.bufAvail = 0;
            buffer.bufPtr = 0;
        }

        JFIFThumbJPEG(Node node) throws IIOInvalidTreeException {
            if (node.getChildNodes().getLength() > 1) {
                throw new IIOInvalidTreeException
                    ("JFIFThumbJPEG node must have 0 or 1 child", node);
            }
            Node child = node.getFirstChild();
            if (child != null) {
                String name = child.getNodeName();
                if (!name.equals("markerSequence")) {
                    throw new IIOInvalidTreeException
                        ("JFIFThumbJPEG child must be a markerSequence node", 
                         node);
                }
                thumbMetadata = new JPEGMetadata(false, true);
                thumbMetadata.setFromMarkerSequenceNode(child);
            }
        }

        protected Object clone() {
            JFIFThumbJPEG newGuy = (JFIFThumbJPEG) super.clone();
            if (thumbMetadata != null) {
                newGuy.thumbMetadata = (JPEGMetadata) thumbMetadata.clone();
            }
            return newGuy;
        }

        IIOMetadataNode getNativeNode() {
            IIOMetadataNode node = new IIOMetadataNode("JFIFthumbJPEG");
            if (thumbMetadata != null) {
                node.appendChild(thumbMetadata.getNativeTree()); 
            }
            return node;
        }

        void print () {
            System.out.println("JFIF thumbnail stored as JPEG");
        }
    }

    /**
     * An APP2 marker segment containing an ICC profile.  In the stream
     * a profile larger than 64K is broken up into a series of chunks.
     * This inner class represents the complete profile as a single objec,
     * combining chunks as necessary.
     */
    class ICCMarkerSegment extends MarkerSegment {
        ArrayList chunks = new ArrayList();
        byte [] profile; // The complete profile when it's fully read
        private static final int ID_SIZE = 12;
        int chunkNum;
        int numChunks;
        
        // FIXME - finish implementation

        ICCMarkerSegment(ICC_ColorSpace cs) {
            super(JPEG.APP2);
            chunks = null;
            chunkNum = 0;
            numChunks = 0;
            profile = cs.getProfile().getData();
        }

        ICCMarkerSegment(JPEGBuffer buffer) throws IOException {
            super(buffer);  // gets whole segment or fills the buffer
            buffer.bufPtr += ID_SIZE; // Skip the id
            buffer.bufAvail -= ID_SIZE;
            /*
             * Reduce the stored length by the id size.  The stored 
             * length is used to store the length of the profile
             * data only.  The ids and chunk numbering bytes are
             * discarded.
             */
            length -= ID_SIZE;
            // get the chunk number, which should be 1
            chunkNum = buffer.buf[buffer.bufPtr++] & 0xff;
            if (chunkNum != 1) {
                throw new IIOException("Image format Error");
            }
            // get the total number of chunks
            numChunks = buffer.buf[buffer.bufPtr++] & 0xff;
            // reduce the stored length by the two chunk numbering bytes
            length -= 2;
            buffer.bufAvail-=2;
            // Now load the rest of the data
            // if there are no more chunks, set up the data
            if (numChunks == 1) {
                data = new byte[length];
                buffer.readData(data);
                inICC = false;
            } else {
                byte [] profileData = new byte[length];
                buffer.readData(profileData);
                chunks.add(profileData);
                inICC = true;
            }
        }

        ICCMarkerSegment(Node node) throws IIOInvalidTreeException {
            super(JPEG.APP2);
            if (node instanceof IIOMetadataNode) {
                IIOMetadataNode ourNode = (IIOMetadataNode) node;
                ICC_Profile prof = (ICC_Profile) ourNode.getUserObject();
                profile = prof.getData();
            } else {
                throw new IIOInvalidTreeException
                    ("app2ICC node must have User Object", node);
            }
        }

        protected Object clone () {
            ICCMarkerSegment newGuy = (ICCMarkerSegment) super.clone();
            if (profile != null) {
                newGuy.profile = (byte[]) profile.clone();
            }
            return newGuy;
        }

        boolean addData(JPEGBuffer buffer) throws IOException {
            // skip the tag
            buffer.bufPtr++;
            buffer.bufAvail--;
            // Get the length, but not in length
            // skip the id
            // get the chunk number, which should be the last plus 1
            // get the number of chunks, which should match
            // if this is not the last chunk
            // read the data
            // inICC = true
            return false;
            // else
            // create an array for the whole thing (in data)
            // copy the existing chunks, releasing them
            // read the final one into the end of the data
            // clear out chunks (= null)
            // inICC = false
            // return true
        }

        IIOMetadataNode getNativeNode() {
            IIOMetadataNode node = new IIOMetadataNode("app2ICC");
            node.setUserObject(ICC_Profile.getInstance(profile));
            return node;
        }

        /**
         * Writes the data for this segment to the stream in
         * valid JPEG format.
         */
        void write(ImageOutputStream ios) throws IOException {
            // FIXME - implement or document as a no-op
        }

        void print () {
            printTag("ICC Profile APP2");
        }
    }
}
    
