/*
 * Copyright (c) 2005, 2017, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.sun.imageio.plugins.tiff;

import java.io.IOException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import javax.imageio.IIOException;
import javax.imageio.stream.MemoryCacheImageInputStream;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.plugins.tiff.BaselineTIFFTagSet;
import javax.imageio.plugins.tiff.TIFFField;

/**
 * {@code TIFFDecompressor} for "Old JPEG" compression.
 */
public class TIFFOldJPEGDecompressor extends TIFFJPEGDecompressor {

    // Define Huffman Tables
    private static final int DHT = 0xC4;

    // Define Quantisation Tables
    private static final int DQT = 0xDB;

    // Define Restart Interval
    private static final int DRI = 0xDD;

    // Baseline DCT
    private static final int SOF0 = 0xC0;

    // Start of Scan
    private static final int SOS = 0xDA;

    // End of Image
    // private static final int EOI = 0xD9; // now defined in superclass

    // Whether the decompressor has been initialized.
    private boolean isInitialized = false;

    //
    // Instance variables set by the initialize() method.
    //
    // Offset to a complete, contiguous JPEG stream.
    private Long JPEGStreamOffset = null;
    // Offset to the SOF marker.
    private int SOFPosition = -1;
    // Value of the SOS marker.
    private byte[] SOSMarker = null;

    // Horizontal chroma subsampling factor.
    private int subsamplingX = 2;

    // Vertical chroma subsampling factor.
    private int subsamplingY = 2;

    public TIFFOldJPEGDecompressor() {}

    //
    // Initialize instance variables according to an analysis of the
    // TIFF field content. See bug 4929147 for test image information.
    //
    // Case 1: Image contains a single strip or tile and the offset to
    //         that strip or tile points to an SOI marker.
    //
    //         Example:
    //         "Visionshape Inc. Compression Software, version 2.5"
    //         ColorTiffWithBarcode.tif
    //         Color2.tif (pages 2-5 (indexes 1-4)
    //         color3.tif (pages 2-5 (indexes 1-4)
    //
    //         "Kofax standard Multi-Page TIFF Storage Filter v2.01.000"
    //         01.tif (pages 1 and 3(indexes 0 and 2))
    //
    //         Instance variables set: JPEGStreamOffset
    //
    // Case 2: Image contains a single strip or tile and a
    //         JPEGInterchangeFormat field is present but the
    //         JPEGInterchangeFormatLength is erroneously missing.
    //
    //         Example:
    //         "Kofax standard Multi-Page TIFF Storage Filter v2.01.000"
    //         01.tif (pages 1 and 3(indexes 0 and 2))
    //         (but this example also satisfies case 1)
    //
    //         Instance variables set: JPEGStreamOffset
    //
    // Case 3: Image contains a single strip or tile, the
    //         JPEGInterchangeFormat and JPEGInterchangeFormatLength
    //         fields are both present, the value of JPEGInterchangeFormat
    //         is less than the offset to the strip or tile, and the sum
    //         of the values of JPEGInterchangeFormat and
    //         JPEGInterchangeFormatLength is greater than the offset to
    //         the strip or tile.
    //
    //         Instance variables set: JPEGStreamOffset
    //
    //         Example:
    //         "HP IL v1.1"
    //         smallliz.tif from libtiff test data.
    //
    //         Instance variables set: JPEGStreamOffset
    //
    // Cases 4-5 apply if none of cases 1-3 applies or the image has multiple
    // strips or tiles.
    //
    // Case 4: JPEGInterchangeFormat and JPEGInterchangeFormatLength are
    //         present, the value of JPEGInterchangeFormatLength is at least 2,
    //         and the sum of the values of these two fields is at most the
    //         value of the offset to the first strip or tile.
    //
    //         Instance variables set: tables, SOFPosition, SOSMarker
    //
    //         Example:
    //         "Oi/GFS, writer v00.06.00P, (c) Wang Labs, Inc. 1990, 1991"
    //         03.tif (pages 1 and 3(indexes 0 and 2))
    //
    //         "Oi/GFS, writer v00.06.02"
    //         Color2.tif (page 1 (index 0))
    //         color3.tif (page 1 (index 0))
    //
    // Case 5: If none of the foregoing cases apply. For this case the
    //         JPEGQTables, JPEGACTables, and JPEGDCTables must be valid.
    //
    //         Instance variables set: tables, SOFPosition, SOSMarker
    //
    //         Example:
    //         "NeXT"
    //         zackthecat.tif from libtiff test data.
    //
    private synchronized void initialize() throws IOException {
        if(isInitialized) {
            return;
        }

        // Get the TIFF metadata object.
        TIFFImageMetadata tim = (TIFFImageMetadata)metadata;

        // Get the JPEGInterchangeFormat field.
        TIFFField JPEGInterchangeFormatField =
            tim.getTIFFField(BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT);

        // Get the tile or strip offsets.
        TIFFField segmentOffsetField =
            tim.getTIFFField(BaselineTIFFTagSet.TAG_TILE_OFFSETS);
        if(segmentOffsetField == null) {
            segmentOffsetField =
                tim.getTIFFField(BaselineTIFFTagSet.TAG_STRIP_OFFSETS);
            if(segmentOffsetField == null) {
                segmentOffsetField = JPEGInterchangeFormatField;
            }
        }
        long[] segmentOffsets = segmentOffsetField.getAsLongs();

            // Determine whether the image has more than one strip or tile.
        boolean isTiled = segmentOffsets.length > 1;

        if(!isTiled) {
            //
            // If the image has only a single strip or tile and it looks
            // as if a complete JPEG stream is present then set the value
            // of JPEGStreamOffset to the offset of the JPEG stream;
            // otherwise leave JPEGStreamOffset set to null.
            //

            stream.seek(offset);
            stream.mark();
            if(stream.read() == 0xff && stream.read() == SOI) {
                // Tile or strip offset points to SOI.
                JPEGStreamOffset = Long.valueOf(offset);

                // Set initialization flag and return.
                ((TIFFImageReader)reader).forwardWarningMessage("SOI marker detected at start of strip or tile.");
                isInitialized = true;
                return;
            }
            stream.reset();

            if(JPEGInterchangeFormatField != null) {
                // Get the value of JPEGInterchangeFormat.
                long jpegInterchangeOffset =
                    JPEGInterchangeFormatField.getAsLong(0);

                // Get the JPEGInterchangeFormatLength field.
                TIFFField JPEGInterchangeFormatLengthField =
                    tim.getTIFFField(BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);

                if(JPEGInterchangeFormatLengthField == null) {
                    // JPEGInterchangeFormat stream is of indeterminate
                    // length so use it as a complete JPEG stream.
                    JPEGStreamOffset = Long.valueOf(jpegInterchangeOffset);

                    // Set initialization flag and return.
                    ((TIFFImageReader)reader).forwardWarningMessage("JPEGInterchangeFormatLength field is missing");
                    isInitialized = true;
                    return;
                } else {
                    // Get the JPEGInterchangeFormatLength field's value.
                    long jpegInterchangeLength =
                        JPEGInterchangeFormatLengthField.getAsLong(0);

                    if(jpegInterchangeOffset < segmentOffsets[0] &&
                       (jpegInterchangeOffset + jpegInterchangeLength) >
                       segmentOffsets[0]) {
                        // JPEGInterchangeFormat points to a position
                        // below the segment start position and ends at
                        // a position after the segment start position.
                        JPEGStreamOffset = Long.valueOf(jpegInterchangeOffset);

                        // Set initialization flag and return.
                        isInitialized = true;
                        return;
                    }
                }
            }
        }

        // Get the subsampling factors.
        TIFFField YCbCrSubsamplingField =
            tim.getTIFFField(BaselineTIFFTagSet.TAG_Y_CB_CR_SUBSAMPLING);
        if(YCbCrSubsamplingField != null) {
            subsamplingX = YCbCrSubsamplingField.getAsChars()[0];
            subsamplingY = YCbCrSubsamplingField.getAsChars()[1];
        }

        //
        // Initialize the 'tables' instance variable either for later
        // use in prepending to individual abbreviated strips or tiles.
        //
        if(JPEGInterchangeFormatField != null) {
            // Get the value of JPEGInterchangeFormat.
            long jpegInterchangeOffset =
                JPEGInterchangeFormatField.getAsLong(0);

            // Get the JPEGInterchangeFormatLength field.
            TIFFField JPEGInterchangeFormatLengthField =
                tim.getTIFFField(BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);

            if(JPEGInterchangeFormatLengthField != null) {
                // Get the JPEGInterchangeFormatLength field's value.
                long jpegInterchangeLength =
                    JPEGInterchangeFormatLengthField.getAsLong(0);

                if(jpegInterchangeLength >= 2 &&
                   jpegInterchangeOffset + jpegInterchangeLength <=
                   segmentOffsets[0]) {
                    // Determine the length excluding any terminal EOI marker
                    // and allocate table memory.
                    stream.mark();
                    stream.seek(jpegInterchangeOffset+jpegInterchangeLength-2);
                    if(stream.read() == 0xff && stream.read() == EOI) {
                        this.tables = new byte[(int)(jpegInterchangeLength-2)];
                    } else {
                        this.tables = new byte[(int)jpegInterchangeLength];
                    }
                    stream.reset();

                    // Read the tables.
                    stream.mark();
                    stream.seek(jpegInterchangeOffset);
                    stream.readFully(tables);
                    stream.reset();

                    ((TIFFImageReader)reader).forwardWarningMessage("Incorrect JPEG interchange format: using JPEGInterchangeFormat offset to derive tables.");
                } else {
                    ((TIFFImageReader)reader).forwardWarningMessage("JPEGInterchangeFormat+JPEGInterchangeFormatLength > offset to first strip or tile.");
                }
            }
        }

        if(this.tables == null) {
            //
            // Create tables-only stream in tables[] consisting of
            // SOI+DQTs+DHTs
            //

            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            // Save stream length;
            long streamLength = stream.length();

            // SOI
            baos.write(0xff);
            baos.write(SOI);

            // Quantization Tables
            TIFFField f =
                tim.getTIFFField(BaselineTIFFTagSet.TAG_JPEG_Q_TABLES);
            if(f == null) {
                throw new IIOException("JPEGQTables field missing!");
            }
            long[] off = f.getAsLongs();

            for(int i = 0; i < off.length; i++) {
                baos.write(0xff); // Marker ID
                baos.write(DQT);

                char markerLength = (char)67;
                baos.write((markerLength >>> 8) & 0xff); // Length
                baos.write(markerLength & 0xff);

                baos.write(i); // Table ID and precision

                byte[] qtable = new byte[64];
                if(streamLength != -1 && off[i] > streamLength) {
                    throw new IIOException("JPEGQTables offset for index "+
                                           i+" is not in the stream!");
                }
                stream.seek(off[i]);
                stream.readFully(qtable);

                baos.write(qtable); // Table data
            }

            // Huffman Tables (k == 0 ? DC : AC).
            for(int k = 0; k < 2; k++) {
                int tableTagNumber = k == 0 ?
                    BaselineTIFFTagSet.TAG_JPEG_DC_TABLES :
                    BaselineTIFFTagSet.TAG_JPEG_AC_TABLES;
                f = tim.getTIFFField(tableTagNumber);
                String fieldName =
                    tableTagNumber ==
                    BaselineTIFFTagSet.TAG_JPEG_DC_TABLES ?
                    "JPEGDCTables" : "JPEGACTables";

                if(f == null) {
                    throw new IIOException(fieldName+" field missing!");
                }
                off = f.getAsLongs();

                for(int i = 0; i < off.length; i++) {
                    baos.write(0xff); // Marker ID
                    baos.write(DHT);

                    byte[] blengths = new byte[16];
                    if(streamLength != -1 && off[i] > streamLength) {
                        throw new IIOException(fieldName+" offset for index "+
                                               i+" is not in the stream!");
                    }
                    stream.seek(off[i]);
                    stream.readFully(blengths);
                    int numCodes = 0;
                    for(int j = 0; j < 16; j++) {
                        numCodes += blengths[j]&0xff;
                    }

                    char markerLength = (char)(19 + numCodes);

                    baos.write((markerLength >>> 8) & 0xff); // Length
                    baos.write(markerLength & 0xff);

                    baos.write(i | (k << 4)); // Table ID and type

                    baos.write(blengths); // Number of codes

                    byte[] bcodes = new byte[numCodes];
                    stream.readFully(bcodes);
                    baos.write(bcodes); // Codes
                }
            }

            // SOF0
            baos.write((byte)0xff); // Marker identifier
            baos.write((byte)SOF0);
            short sval = (short)(8 + 3*samplesPerPixel); // Length
            baos.write((byte)((sval >>> 8) & 0xff));
            baos.write((byte)(sval & 0xff));
            baos.write((byte)8); // Data precision
            sval = (short)srcHeight; // Tile/strip height
            baos.write((byte)((sval >>> 8) & 0xff));
            baos.write((byte)(sval & 0xff));
            sval = (short)srcWidth; // Tile/strip width
            baos.write((byte)((sval >>> 8) & 0xff));
            baos.write((byte)(sval & 0xff));
            baos.write((byte)samplesPerPixel); // Number of components
            if(samplesPerPixel == 1) {
                baos.write((byte)1); // Component ID
                baos.write((byte)0x11); // Subsampling factor
                baos.write((byte)0); // Quantization table ID
            } else { // 3
                for(int i = 0; i < 3; i++) {
                    baos.write((byte)(i + 1)); // Component ID
                    baos.write((i != 0) ?
                               (byte)0x11 :
                               (byte)(((subsamplingX & 0x0f) << 4) |
                                      (subsamplingY & 0x0f)));

                    baos.write((byte)i); // Quantization table ID
                }
            };


            // DRI (optional).
            f = tim.getTIFFField(BaselineTIFFTagSet.TAG_JPEG_RESTART_INTERVAL);
            if(f != null) {
                char restartInterval = f.getAsChars()[0];

                if(restartInterval != 0) {
                    baos.write((byte)0xff); // Marker identifier
                    baos.write((byte)DRI);

                    sval = 4;
                    baos.write((byte)((sval >>> 8) & 0xff)); // Length
                    baos.write((byte)(sval & 0xff));

                    // RestartInterval
                    baos.write((byte)((restartInterval >>> 8) & 0xff));
                    baos.write((byte)(restartInterval & 0xff));
                }
            }

            tables = baos.toByteArray();
        }

        //
        // Check for presence of SOF marker and save its position.
        //
        int idx = 0;
        int idxMax = tables.length - 1;
        while(idx < idxMax) {
            if((tables[idx]&0xff) == 0xff &&
               (tables[idx+1]&0xff) == SOF0) {
                SOFPosition = idx;
                break;
            }
            idx++;
        }

        //
        // If no SOF marker, add one.
        //
        if(SOFPosition == -1) {
            byte[] tmpTables =
                new byte[tables.length + 10 + 3*samplesPerPixel];
            System.arraycopy(tables, 0, tmpTables, 0, tables.length);
            int tmpOffset = tables.length;
            SOFPosition = tables.length;
            tables = tmpTables;

            tables[tmpOffset++] = (byte)0xff; // Marker identifier
            tables[tmpOffset++] = (byte)SOF0;
            short sval = (short)(8 + 3*samplesPerPixel); // Length
            tables[tmpOffset++] = (byte)((sval >>> 8) & 0xff);
            tables[tmpOffset++] = (byte)(sval & 0xff);
            tables[tmpOffset++] = (byte)8; // Data precision
            sval = (short)srcHeight; // Tile/strip height
            tables[tmpOffset++] = (byte)((sval >>> 8) & 0xff);
            tables[tmpOffset++] = (byte)(sval & 0xff);
            sval = (short)srcWidth; // Tile/strip width
            tables[tmpOffset++] = (byte)((sval >>> 8) & 0xff);
            tables[tmpOffset++] = (byte)(sval & 0xff);
            tables[tmpOffset++] = (byte)samplesPerPixel; // Number of components
            if(samplesPerPixel == 1) {
                tables[tmpOffset++] = (byte)1; // Component ID
                tables[tmpOffset++] = (byte)0x11; // Subsampling factor
                tables[tmpOffset++] = (byte)0; // Quantization table ID
            } else { // 3
                for(int i = 0; i < 3; i++) {
                    tables[tmpOffset++] = (byte)(i + 1); // Component ID
                    tables[tmpOffset++] = (i != 0) ?
                        (byte)0x11 :
                        (byte)(((subsamplingX & 0x0f) << 4) |
                               (subsamplingY & 0x0f));

                    tables[tmpOffset++] = (byte)i; // Quantization table ID
                }
            };
        }

        //
        // Initialize SOSMarker.
        //
        stream.mark();
        stream.seek(segmentOffsets[0]);
        if(stream.read() == 0xff && stream.read() == SOS) {
            //
            // If the first segment starts with an SOS marker save it.
            //
            int SOSLength = (stream.read()<<8)|stream.read();
            SOSMarker = new byte[SOSLength+2];
            SOSMarker[0] = (byte)0xff;
            SOSMarker[1] = (byte)SOS;
            SOSMarker[2] = (byte)((SOSLength & 0xff00) >> 8);
            SOSMarker[3] = (byte)(SOSLength & 0xff);
            stream.readFully(SOSMarker, 4, SOSLength - 2);
        } else {
            //
            // Manufacture an SOS marker.
            //
            SOSMarker = new byte[2 + 6 + 2*samplesPerPixel];
            int SOSMarkerIndex = 0;
            SOSMarker[SOSMarkerIndex++] = (byte)0xff; // Marker identifier
            SOSMarker[SOSMarkerIndex++] = (byte)SOS;
            short sval = (short)(6 + 2*samplesPerPixel); // Length
            SOSMarker[SOSMarkerIndex++] = (byte)((sval >>> 8) & 0xff);
            SOSMarker[SOSMarkerIndex++] = (byte)(sval & 0xff);
            // Number of components in scan
            SOSMarker[SOSMarkerIndex++] = (byte)samplesPerPixel;
            if(samplesPerPixel == 1) {
                SOSMarker[SOSMarkerIndex++] = (byte)1; // Component ID
                SOSMarker[SOSMarkerIndex++] = (byte)0; // Huffman table ID
            } else { // 3
                for(int i = 0; i < 3; i++) {
                    SOSMarker[SOSMarkerIndex++] =
                        (byte)(i + 1); // Component ID
                    SOSMarker[SOSMarkerIndex++] =
                        (byte)((i << 4) | i); // Huffman table IDs
                }
            };
            SOSMarker[SOSMarkerIndex++] = (byte)0;
            SOSMarker[SOSMarkerIndex++] = (byte)0x3f;
            SOSMarker[SOSMarkerIndex++] = (byte)0;
        }
        stream.reset();

        // Set initialization flag.
        isInitialized = true;
    }

    //
    // The strategy for reading cases 1-3 is to treat the data as a complete
    // JPEG interchange stream located at JPEGStreamOffset.
    //
    // The strategy for cases 4-5 is to concatenate a tables stream created
    // in initialize() with the entropy coded data in each strip or tile.
    //
    public void decodeRaw(byte[] b,
                          int dstOffset,
                          int bitsPerPixel,
                          int scanlineStride) throws IOException {

        initialize();

        TIFFImageMetadata tim = (TIFFImageMetadata)metadata;

        if(JPEGStreamOffset != null) {
            stream.seek(JPEGStreamOffset.longValue());
            JPEGReader.setInput(stream, false, true);
        } else {
            // Determine buffer length and allocate.
            int tableLength = tables.length;
            int bufLength =
                tableLength + SOSMarker.length + byteCount + 2; // 2 for EOI.
            byte[] buf = new byte[bufLength];
            System.arraycopy(tables, 0, buf, 0, tableLength);
            int bufOffset = tableLength;

            // Update the SOF dimensions.
            short sval = (short)srcHeight; // Tile/strip height
            buf[SOFPosition + 5] = (byte)((sval >>> 8) & 0xff);
            buf[SOFPosition + 6] = (byte)(sval & 0xff);
            sval = (short)srcWidth; // Tile/strip width
            buf[SOFPosition + 7] = (byte)((sval >>> 8) & 0xff);
            buf[SOFPosition + 8] = (byte)(sval & 0xff);

            // Seek to data.
            stream.seek(offset);

            // Add SOS marker if data segment does not start with one.
            byte[] twoBytes = new byte[2];
            stream.readFully(twoBytes);
            if(!((twoBytes[0]&0xff) == 0xff && (twoBytes[1]&0xff) == SOS)) {
                // Segment does not start with SOS marker;
                // use the pre-calculated SOS marker.
                System.arraycopy(SOSMarker, 0, buf, bufOffset,
                                 SOSMarker.length);
                bufOffset += SOSMarker.length;
            }

            // Copy the segment data into the buffer.
            buf[bufOffset++] = twoBytes[0];
            buf[bufOffset++] = twoBytes[1];
            stream.readFully(buf, bufOffset, byteCount - 2);
            bufOffset += byteCount - 2;

            // EOI.
            buf[bufOffset++] = (byte)0xff; // Marker identifier
            buf[bufOffset++] = (byte)EOI;

            ByteArrayInputStream bais =
                new ByteArrayInputStream(buf, 0, bufOffset);
            ImageInputStream is = new MemoryCacheImageInputStream(bais);

            JPEGReader.setInput(is, true, true);
        }

        // Read real image
        JPEGParam.setDestination(rawImage);
        JPEGReader.read(0, JPEGParam);
    }

    @SuppressWarnings("removal")
    protected void finalize() throws Throwable {
        super.finalize();
        JPEGReader.dispose();
    }
}
