/* Copyright (C) 2005-2011 Fabio Riccardi */

package com.lightcrafts.image.libs;

import java.awt.*;
import java.awt.image.*;
import java.awt.color.ColorSpace;
import java.io.*;
import java.nio.ByteBuffer;
import com.lightcrafts.mediax.jai.PlanarImage;
import com.lightcrafts.mediax.jai.ImageLayout;

import com.lightcrafts.jai.JAIContext;
import com.lightcrafts.jai.opimage.CachedImage;
import com.lightcrafts.image.types.AdobeJPEGSegmentFilter;
import com.lightcrafts.image.types.JPEGImageInfo;
import com.lightcrafts.utils.thread.ProgressThread;
import com.lightcrafts.utils.ProgressIndicator;
import com.lightcrafts.utils.UserCanceledException;

import static com.lightcrafts.image.types.JPEGConstants.*;

/**
 * An <code>LCJPEGReader</code> is a Java wrapper around the LibJPEG library
 * for reading JPEG images.
 *
 * @author Paul J. Lucas [paul@lightcrafts.com]
 * @see <a href="http://www.ijg.org/">LibJPEG</a>
 */
public final class LCJPEGReader {

    ////////// public /////////////////////////////////////////////////////////

    /**
     * The default buffer size for use with
     * {@link #LCJPEGReader(LCImageDataProvider).
     */
    public static final int DEFAULT_BUF_SIZE = 32 * 1024;

    /**
     * Construct an <code>LCJPEGReader</code>.
     *
     * @param fileName The name of the JPEG file to read.
     */
    public LCJPEGReader( String fileName )
        throws FileNotFoundException, LCImageLibException
    {
        this( fileName, 0, 0, null );
    }

    /**
     * Construct an <code>LCJPEGReader</code>.
     *
     * @param fileName The name of the JPEG file to read.
     * @param maxWidth The maximum width of the image to get, rescaling if
     * necessary.  A value of 0 means don't scale.
     * @param maxHeight The maximum height of the image to get, rescaling if
     * necessary.  A value of 0 means don't scale.
     * @param jpegInfo The {@link JPEGImageInfo} of the image, or
     * <code>null</code>.
     */
    public LCJPEGReader( String fileName, int maxWidth, int maxHeight,
                         JPEGImageInfo jpegInfo )
        throws FileNotFoundException, LCImageLibException
    {
        openForReading( fileName, maxWidth, maxHeight );
        if ( jpegInfo != null ) {
            final ByteBuffer segBuf = jpegInfo.getFirstSegmentFor(
                JPEG_APPE_MARKER, new AdobeJPEGSegmentFilter()
            );
            m_hasAdobeSegment = segBuf != null;
        }
    }

    /**
     * Construct an <code>LCJPEGReader</code>.
     *
     * @param provider The {@link LCImageDataProvider} to get image data from.
     */
    public LCJPEGReader( LCImageDataProvider provider )
        throws LCImageLibException
    {
        this( provider, 0, 0 );
    }

    /**
     * Construct an <code>LCJPEGReader</code>.
     *
     * @param provider The {@link LCImageDataProvider} to get image data from.
     * @param maxWidth The maximum width of the image to get, rescaling if
     * necessary.  A value of 0 means don't scale.
     * @param maxHeight The maximum height of the image to get, rescaling if
     * necessary.  A value of 0 means don't scale.
     */
    public LCJPEGReader( LCImageDataProvider provider, int maxWidth,
                         int maxHeight )
        throws LCImageLibException
    {
        beginRead( provider, DEFAULT_BUF_SIZE, maxWidth, maxHeight );
    }

    /**
     * Construct an <code>LCJPEGReader</code>.
     *
     * @param provider The {@link LCImageDataProvider} to get image data from.
     * @param bufSize The size of the buffer (in bytes) to use.
     */
    public LCJPEGReader( LCImageDataProvider provider, int bufSize )
        throws LCImageLibException
    {
        beginRead( provider, bufSize, 0, 0 );
    }

    /**
     * Construct an <code>LCJPEGReader</code>.
     *
     * @param provider The {@link LCImageDataProvider} to get image data from.
     * @param bufSize The size of the buffer to use.
     * @param maxWidth The maximum width of the image to get, rescaling if
     * necessary.  A value of 0 means don't scale.
     * @param maxHeight The maximum height of the image to get, rescaling if
     * necessary.  A value of 0 means don't scale.
     */
    public LCJPEGReader( LCImageDataProvider provider, int bufSize,
                         int maxWidth, int maxHeight )
        throws LCImageLibException
    {
        beginRead( provider, bufSize, maxWidth, maxHeight );
    }

    /**
     * Dispose of an <code>LCJPEGReader</code>.
     */
    public native void dispose() throws LCImageLibException;

    /**
     * Gets the number of color components per pixel.
     *
     * @return Returns said number.
     */
    public int getColorsPerPixel() {
        return m_colorsPerPixel;
    }

    /**
     * Gets the colorspace of the image.
     *
     * @return Returns one of: <code>CS_GRAYSCALE</code>, <code>CS_RGB</code>,
     * <code>CS_YCbRr</code>, <code>CS_CMYK</code>, <code>CS_YCCK</code>, or
     * <code>CS_UNKNOWN</code>.
     */
    public int getColorSpace() {
        return m_colorSpace;
    }

    /**
     * Gets the height of the image in pixels.
     *
     * @return Returns said height.
     */
    public int getHeight() {
        return m_height;
    }

    /**
     * Gets the JPEG image.
     *
     * @return Returns said image.
     */
    public PlanarImage getImage()
        throws LCImageLibException, UserCanceledException
    {
        return getImage( null, null );
    }

    /**
     * Gets the JPEG image.
     *
     * @param cs The {@link ColorSpace} to use.
     * @return Returns said image.
     */
    public synchronized PlanarImage getImage( ColorSpace cs )
        throws LCImageLibException, UserCanceledException
    {
        return getImage( null, cs );
    }

    /**
     * Gets the JPEG image.
     *
     * @param thread The thread that will do the getting.
     * @param cs The {@link ColorSpace} to use.
     * @return Returns said image.
     */
    public synchronized PlanarImage getImage( ProgressThread thread,
                                              ColorSpace cs )
        throws LCImageLibException, UserCanceledException
    {
        if ( m_image == null ) {
            boolean userCanceled = false;
            try {
                readImage( thread, cs );
            }
            catch ( UserCanceledException e ) {
                userCanceled = true;
                throw e;
            }
            finally {
                try {
                    dispose();
                }
                catch ( LCImageLibException e ) {
                    //
                    // The JPEG library will complain if dispose() is called
                    // before the entire image has been read ("Application
                    // transferred too few scanlines") because the user clicked
                    // the "Cancel" button.  Therefore, ignore any exception if
                    // this is the case, but rethrow it otherwise.
                    //
                    if ( !userCanceled )
                        throw e;
                }
            }
        }
        return m_image;
    }

    /**
     * Gets the width of the image in pixels.
     *
     * @return Returns said width.
     */
    public int getWidth() {
        return m_width;
    }

    /**
     * Reads and decodes and encoded set of scanlines from the JPEG image.
     *
     * @param buf The buffer into which to read the image data.
     * @param offset The offset into the buffer where the image data will begin
     * being placed.
     * @param numLines The number of scanlines to read.
     * @return Returns the number of scanlines read.
     */
    public native synchronized int readScanLines( byte[] buf, long offset,
                                                  int numLines )
        throws LCImageLibException;

    ////////// protected //////////////////////////////////////////////////////

    /**
     * Finalize this class by calling {@link #dispose()}.
     */
    protected void finalize() throws Throwable {
        super.finalize();
        dispose();
    }

    ////////// private ////////////////////////////////////////////////////////

    /**
     * Begin using the {@link LCImageDataProvider} to get JPEG image data.
     *
     * @param provider The {@link LCImageDataProvider} to get image data from.
     * @param bufSize The size of the buffer to use.
     * @param maxWidth The maximum width of the image to get, rescaling if
     * necessary.  A value of 0 means don't scale.
     * @param maxHeight The maximum height of the image to get, rescaling if
     * necessary.  A value of 0 means don't scale.
     */
    private native void beginRead( LCImageDataProvider provider, int bufSize,
                                   int maxWidth, int maxHeight )
        throws LCImageLibException;

    /**
     * Open a JPEG file for reading.
     *
     * @param fileName The name of the JPEG file to open.
     * @param maxWidth The maximum width of the image to get, rescaling if
     * necessary.  A value of 0 means don't scale.
     * @param maxHeight The maximum height of the image to get, rescaling if
     * necessary.  A value of 0 means don't scale.
     */
    private native void openForReading( String fileName, int maxWidth,
                                        int maxHeight )
        throws FileNotFoundException, LCImageLibException;

    /**
     * Reads the JPEG image.
     *
     * @param thread The thread that will do the getting.
     * @param cs The {@link ColorSpace} to use.
     */
    private void readImage( ProgressThread thread, ColorSpace cs )
        throws LCImageLibException, UserCanceledException
    {
        ProgressIndicator indicator = null;
        if ( thread != null )
            indicator = thread.getProgressIndicator();
        if ( indicator != null )
            indicator.setMaximum( m_height );

        // todo: deal with color models other than rgb and grayscale

        if ( cs == null )
            cs = (m_colorsPerPixel == 1 ? JAIContext.gray22ColorSpace :
                  m_colorsPerPixel == 3 ? JAIContext.sRGBColorSpace :
                                          JAIContext.CMYKColorSpace);

        // Color model for the image (and everything else).
        final ComponentColorModel ccm = new ComponentColorModel(
            cs, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE
        );

        // Sample model for the readout buffer large enough to hold a tile or a
        // strip of the image.
        final SampleModel jpegTsm = ccm.createCompatibleSampleModel(
            m_width, JAIContext.TILE_HEIGHT
        );

        // The readout buffer itself.
        final DataBuffer db = new DataBufferByte(
            m_colorsPerPixel * m_width * JAIContext.TILE_HEIGHT
        );

        // Sample model for the output image.
        final SampleModel tsm = ccm.createCompatibleSampleModel(
            JAIContext.TILE_WIDTH, JAIContext.TILE_HEIGHT
        );

        // Layout of the output image.
        final ImageLayout layout = new ImageLayout(
            0, 0, m_width, m_height, 0, 0, JAIContext.TILE_WIDTH,
            JAIContext.TILE_HEIGHT, tsm, ccm
        );

        // The output image itself, directly allocated in the file cache.
        final CachedImage image =
            new CachedImage( layout, JAIContext.fileCache );

        // Load Image Data
        int tileY = 0;
        int totalLinesRead = 0;
        while ( totalLinesRead < m_height ) {
            if ( thread != null && thread.isCanceled() )
                throw new UserCanceledException();
            final int tileHeight =
                Math.min( JAIContext.TILE_HEIGHT, m_height - totalLinesRead );

            // Wrap the data buffer with a Raster representing the input data.
            final WritableRaster raster = Raster.createWritableRaster(
                jpegTsm, db, new Point( 0, tileY * JAIContext.TILE_HEIGHT )
            );

            final int linesRead = readScanLines(
                ((DataBufferByte)db).getData(), 0, tileHeight
            );
            if ( linesRead <= 0 ) {
                System.out.println("Problem with readScanLines, returned: " + linesRead );
                break;
            }

            if ( m_hasAdobeSegment && m_colorsPerPixel == 4 ) {
                //
                // CMYK JPEG images generated by Photoshop are inverted, so we
                // have to invert the data to make it look right.
                //
                final byte data[] = ((DataBufferByte) db).getData();
                for (int i = 0; i < data.length; i++)
                    data[i] = (byte)~data[i];
            }

            totalLinesRead += linesRead;
            tileY++;

            image.setData( raster );

            if ( indicator != null )
                indicator.incrementBy( linesRead );
        }
        if ( indicator != null )
            indicator.setIndeterminate( true );
        m_image = image;
    }

    /**
     * The number of colors per pixel.  This is set from native code.
     */
    @SuppressWarnings({"UNUSED_SYMBOL"})
    private int m_colorsPerPixel;

    /**
     * The colorspace of the image, one of: <code>CS_GRAYSCALE</code>,
     * <code>CS_RGB</code>, <code>CS_YCbRr</code>, <code>CS_CMYK</code>,
     * <code>CS_YCCK</code>, or <code>CS_UNKNOWN</code>.  This is set from
     * native code.
     */
    @SuppressWarnings({"UNUSED_SYMBOL"})
    private int m_colorSpace;

    /**
     * This is <code>true</code> only if the JPEG file has an Adobe (APPE)
     * segment.
     */
    private boolean m_hasAdobeSegment;

    /**
     * The image height.  This is set from native code.
     */
    @SuppressWarnings({"UNUSED_SYMBOL"})
    private int m_height;

    /**
     * The actual end-result image.
     */
    private PlanarImage m_image;

    /**
     * This is where the native code stores a pointer to the <code>JPEG</code>
     * native data structure.  Do not touch this from Java except to compare it
     * to zero.
     */
    @SuppressWarnings({"UNUSED_SYMBOL"})
    private long m_nativePtr;

    /**
     * The image width.  This is set from native code.
     */
    @SuppressWarnings({"UNUSED_SYMBOL"})
    private int m_width;

    static {
        System.loadLibrary( "LCJPEG" );
    }
}
/* vim:set et sw=4 ts=4: */
