/*
 * Copyright (c) 2005, 2025, 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 javax.imageio.plugins.tiff.BaselineTIFFTagSet;
import javax.imageio.plugins.tiff.TIFFField;
import javax.imageio.plugins.tiff.TIFFTag;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Iterator;
import javax.imageio.ImageReader;
import javax.imageio.ImageWriteParam;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.IIORegistry;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.spi.ServiceRegistry;
import javax.imageio.stream.MemoryCacheImageInputStream;
import javax.imageio.stream.MemoryCacheImageOutputStream;

/**
 * Compressor for encoding compression type 7, TTN2/Adobe JPEG-in-TIFF.
 */
public class TIFFJPEGCompressor extends TIFFBaseJPEGCompressor {

    // Subsampling factor for chroma bands (Cb Cr).
    static final int CHROMA_SUBSAMPLING = 2;

    /**
     * A filter which identifies the ImageReaderSpi of a JPEG reader
     * which supports JPEG native stream metadata.
     */
    private static class JPEGSPIFilter implements ServiceRegistry.Filter {
        JPEGSPIFilter() {}

        @Override
        public boolean filter(Object provider) {
            ImageReaderSpi readerSPI = (ImageReaderSpi)provider;

            if(readerSPI != null) {
                String streamMetadataName =
                    readerSPI.getNativeStreamMetadataFormatName();
                if(streamMetadataName != null) {
                    return streamMetadataName.equals(STREAM_METADATA_NAME);
                } else {
                    return false;
                }
            }

            return false;
        }
    }

    /**
     * Retrieves a JPEG reader which supports native JPEG stream metadata.
     */
    private static ImageReader getJPEGTablesReader() {
        ImageReader jpegReader = null;

        try {
            IIORegistry registry = IIORegistry.getDefaultInstance();
            Iterator<?> readerSPIs =
                registry.getServiceProviders(ImageReaderSpi.class,
                                             new JPEGSPIFilter(),
                                             true);
            if(readerSPIs.hasNext()) {
                ImageReaderSpi jpegReaderSPI =
                    (ImageReaderSpi)readerSPIs.next();
                jpegReader = jpegReaderSPI.createReaderInstance();
            }
        } catch(Exception e) {
            // Ignore it ...
        }

        return jpegReader;
    }

    public TIFFJPEGCompressor(ImageWriteParam param) {
        super("JPEG", BaselineTIFFTagSet.COMPRESSION_JPEG, false, param);
    }

    /**
     * Sets the value of the {@code metadata} field.
     *
     * <p>The implementation in this class also adds the TIFF fields
     * JPEGTables, YCbCrSubSampling, YCbCrPositioning, and
     * ReferenceBlackWhite superseding any prior settings of those
     * fields.</p>
     *
     * @param metadata the {@code IIOMetadata} object for the
     * image being written.
     *
     * @see #getMetadata()
     */
    @Override
    public void setMetadata(IIOMetadata metadata) {
        super.setMetadata(metadata);

        if (metadata instanceof TIFFImageMetadata) {
            TIFFImageMetadata tim = (TIFFImageMetadata)metadata;
            TIFFIFD rootIFD = tim.getRootIFD();
            BaselineTIFFTagSet base = BaselineTIFFTagSet.getInstance();

            TIFFField f =
                tim.getTIFFField(BaselineTIFFTagSet.TAG_SAMPLES_PER_PIXEL);
            int numBands = f.getAsInt(0);

            if(numBands == 1) {
                // Remove YCbCr fields not relevant for grayscale.

                rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_Y_CB_CR_SUBSAMPLING);
                rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_Y_CB_CR_POSITIONING);
                rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_REFERENCE_BLACK_WHITE);
            } else { // numBands == 3
                // Replace YCbCr fields.

                // YCbCrSubSampling
                TIFFField YCbCrSubSamplingField = new TIFFField
                    (base.getTag(BaselineTIFFTagSet.TAG_Y_CB_CR_SUBSAMPLING),
                     TIFFTag.TIFF_SHORT, 2,
                     new char[] {CHROMA_SUBSAMPLING, CHROMA_SUBSAMPLING});
                rootIFD.addTIFFField(YCbCrSubSamplingField);

                // YCbCrPositioning
                TIFFField YCbCrPositioningField = new TIFFField
                    (base.getTag(BaselineTIFFTagSet.TAG_Y_CB_CR_POSITIONING),
                     TIFFTag.TIFF_SHORT, 1,
                     new char[]
                        {BaselineTIFFTagSet.Y_CB_CR_POSITIONING_CENTERED});
                rootIFD.addTIFFField(YCbCrPositioningField);

                // ReferenceBlackWhite
                TIFFField referenceBlackWhiteField = new TIFFField
                    (base.getTag(BaselineTIFFTagSet.TAG_REFERENCE_BLACK_WHITE),
                     TIFFTag.TIFF_RATIONAL, 6,
                     new long[][] { // no headroon/footroom
                         {0, 1}, {255, 1},
                         {128, 1}, {255, 1},
                         {128, 1}, {255, 1}
                     });
                rootIFD.addTIFFField(referenceBlackWhiteField);
            }

            // JPEGTables field is written if and only if one is
            // already present in the metadata. If one is present
            // and has either zero length or does not represent a
            // valid tables-only stream, then a JPEGTables field
            // will be written initialized to the standard tables-
            // only stream written by the JPEG writer.

            // Retrieve the JPEGTables field.
            TIFFField JPEGTablesField =
                tim.getTIFFField(BaselineTIFFTagSet.TAG_JPEG_TABLES);

            // Initialize JPEG writer to one supporting abbreviated streams.
            if(JPEGTablesField != null) {
                // Initialize the JPEG writer to one that supports stream
                // metadata, i.e., abbreviated streams, and may or may not
                // support image metadata.
                initJPEGWriter(true, false);
            }

            // Write JPEGTables field if a writer supporting abbreviated
            // streams was available.
            if(JPEGTablesField != null && JPEGWriter != null) {
                // Set the abbreviated stream flag.
                this.writeAbbreviatedStream = true;

                //Branch based on field value count.
                if(JPEGTablesField.getCount() > 0) {
                    // Derive the stream metadata from the field.

                    // Get the field values.
                    byte[] tables = JPEGTablesField.getAsBytes();

                    // Create an input stream for the tables.
                    ByteArrayInputStream bais =
                        new ByteArrayInputStream(tables);
                    MemoryCacheImageInputStream iis =
                        new MemoryCacheImageInputStream(bais);

                    // Read the tables stream using the JPEG reader.
                    ImageReader jpegReader = getJPEGTablesReader();
                    jpegReader.setInput(iis);

                    // Initialize the stream metadata object.
                    try {
                        JPEGStreamMetadata = jpegReader.getStreamMetadata();
                    } catch(Exception e) {
                        // Fall back to default tables.
                        JPEGStreamMetadata = null;
                    } finally {
                        jpegReader.reset();
                    }
                }

                if(JPEGStreamMetadata == null) {
                    // Derive the field from default stream metadata.

                    // Get default stream metadata.
                    JPEGStreamMetadata =
                        JPEGWriter.getDefaultStreamMetadata(JPEGParam);

                    // Create an output stream for the tables.
                    ByteArrayOutputStream tableByteStream =
                        new ByteArrayOutputStream();
                    MemoryCacheImageOutputStream tableStream =
                        new MemoryCacheImageOutputStream(tableByteStream);

                    // Write a tables-only stream.
                    JPEGWriter.setOutput(tableStream);
                    try {
                        JPEGWriter.prepareWriteSequence(JPEGStreamMetadata);
                        tableStream.flush();
                        JPEGWriter.endWriteSequence();

                        // Get the tables-only stream content.
                        byte[] tables = tableByteStream.toByteArray();

                        // Add the JPEGTables field.
                        JPEGTablesField = new TIFFField
                            (base.getTag(BaselineTIFFTagSet.TAG_JPEG_TABLES),
                             TIFFTag.TIFF_UNDEFINED,
                             tables.length,
                             tables);
                        rootIFD.addTIFFField(JPEGTablesField);
                    } catch(Exception e) {
                        // Do not write JPEGTables field.
                        rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_TABLES);
                        this.writeAbbreviatedStream = false;
                    }
                }
            } else { // Do not write JPEGTables field.
                // Remove any field present.
                rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_TABLES);

                // Initialize the writer preferring codecLib.
                initJPEGWriter(false, false);
            }
        }
    }
}
