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

package com.lightcrafts.image.metadata;

import java.io.File;
import java.io.IOException;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import com.lightcrafts.utils.bytebuffer.LCByteBuffer;
import com.lightcrafts.image.ImageInfo;
import com.lightcrafts.image.BadImageFileException;
import com.lightcrafts.image.metadata.makernotes.MakerNotesDirectory;
import com.lightcrafts.image.metadata.makernotes.MakerNoteProbe;
import com.lightcrafts.image.metadata.values.ImageMetaValue;

import static com.lightcrafts.image.metadata.EXIFTags.*;
import static com.lightcrafts.image.metadata.ImageMetadataConstants.DIRECTORY_ENTRY_MAX_SANE_SIZE;

/**
 * An <code>EXIFMetadataReader</code> is-an {@link ImageMetadataReader} that
 * reads EXIF metadata directories.
 *
 * @author Paul J. Lucas [paul@lightcrafts.com]
 */
public class EXIFMetadataReader extends ImageMetadataReader
    implements EXIFParserEventHandler {

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

    /**
     *
     * @param imageInfo
     * @param exifSegBuf The {@link LCByteBuffer} containing the raw bytes of
     * @param isSubdirectory
     */
    public EXIFMetadataReader( ImageInfo imageInfo, LCByteBuffer exifSegBuf,
                               boolean isSubdirectory ) {
        super( imageInfo, exifSegBuf );
        m_exifParser = new EXIFParser(
            this, imageInfo.getFile(), exifSegBuf, isSubdirectory
        );
    }

    /**
     * Read the metadata from a single directory.
     *
     * @param offset The offset from the beginning of the file of the
     * directory.
     * @param valueOffsetAdjustment The larger-than-4-byte-value offset
     * adjustment.
     * @param dir The metadata is put here.
     */
    public void readDirectory( int offset, int valueOffsetAdjustment,
                               ImageMetadataDirectory dir ) throws IOException {
        m_exifParser.parseDirectory( offset, valueOffsetAdjustment, dir );
    }

    ////////// EXIFMetadataReader methods /////////////////////////////////////

    /**
     * {@inheritDoc}
     */
    public void readAllDirectories() throws IOException {
        m_exifParser.parseAllDirectories();
        mergeEXIFDirectories();
    }

    /**
     * {@inheritDoc}
     */
    public void readHeader() throws BadImageFileException, IOException {
        m_exifParser.parseHeader();
    }

    ////////// EXIFParserEventHandler methods /////////////////////////////////

    /**
     * {@inheritDoc}
     */
    public void gotBadMetadata( String message ) {
        logBadImageMetadata( message );
    }

    /**
     * {@inheritDoc}
     */
    public void gotBadMetadata( Throwable cause ) {
        logBadImageMetadata( cause );
    }

    /**
     * {@inheritDoc}
     */
    public ImageMetadataDirectory gotDirectory() {
        ++m_ifdIndex;
        return createDirectoryFor( EXIFDirectory.class, "IFD" );
    }

    /**
     * {@inheritDoc}
     */
    public void gotTag( int tagID, int fieldType, int numValues, int byteCount,
                        int valueOffset, int valueOffsetAdjustment,
                        int subdirOffset, File imageFile, LCByteBuffer buf,
                        ImageMetadataDirectory dir )
        throws IOException
    {
        switch ( tagID ) {
            case EXIF_GPS_IFD_POINTER:
                final ImageMetadataDirectory gpsDir =
                    m_metadata.getDirectoryFor( GPSDirectory.class, true );
                m_exifParser.parseDirectory( subdirOffset, 0, gpsDir );
                return;
            case EXIF_IFD_POINTER:
                m_exifParser.parseDirectory( subdirOffset, 0, dir );
                return;
            case EXIF_INTEROPERABILITY_POINTER:
                // TODO: handle this case
                return;
            case EXIF_MAKER_NOTE:
                final Class<? extends MakerNotesDirectory> notesClass =
                    MakerNoteProbe.determineMakerNotesFrom( m_metadata );
                if ( notesClass != null ) {
                    final ImageMetadataDirectory notesDir =
                        m_metadata.getDirectoryFor( notesClass, true );
                    readMakerNotes( valueOffset, byteCount, notesDir );
                }
                return;
            case EXIF_USER_COMMENT:
                // TODO: handle this case
                return;
        }

        if ( byteCount > DIRECTORY_ENTRY_MAX_SANE_SIZE )
            return;
        final ImageMetaValue value = m_exifParser.parseValue(
            tagID, fieldType, valueOffset, numValues
        );
        if ( value != null )
            try {
                dir.putValue( tagID, value );
            }
            catch ( IllegalArgumentException e ) {
                gotBadMetadata( e );
            }
    }

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

    /** The {@link EXIFParser} in use. */
    protected final EXIFParser m_exifParser;

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

    /**
     * Copy metadata values from a TIFF {@link ImageMetadataDirectory} that is
     * not for the full-sized image, e.g., a preview or a thumbnail, to the
     * TIFF {@link ImageMetadataDirectory} for the full-sized image.
     * <p>
     * Note that the copy is a partial copy, i.e., it doesn't copy all tags.
     * It only copies only those that could be useful for the full-sized image
     * (like ARTIST, MAKE, and MODEL), i.e., tags that aren't specific to the
     * non-full-sized image.
     *
     * @param fromDir The {@link ImageMetadataDirectory} to copy values from.
     * @param toDir The {@link ImageMetadataDirectory} to copy values to.
     */
    private static void copyValuesFromTo( ImageMetadataDirectory fromDir,
                                          ImageMetadataDirectory toDir ) {
        for ( Iterator<Map.Entry<Integer,ImageMetaValue>>
              i = fromDir.iterator(); i.hasNext(); ) {
            final Map.Entry<Integer,ImageMetaValue> me = i.next();
            final int tagID = me.getKey();
            switch ( tagID ) {
                case EXIF_BITS_PER_SAMPLE:
                case EXIF_CFA_PATTERN:
                case EXIF_COLOR_SPACE:
                case EXIF_COMPONENTS_CONFIGURATION:
                case EXIF_COMPRESSED_BITS_PER_PIXEL:
                case EXIF_COMPRESSION:
                case EXIF_GPS_IFD_POINTER:
                case EXIF_ICC_PROFILE:
                case EXIF_IFD_POINTER:
                case EXIF_IMAGE_HEIGHT:
                case EXIF_IMAGE_WIDTH:
                case EXIF_INTEROPERABILITY_POINTER:
                case EXIF_MAKER_NOTE:
                case EXIF_NEW_SUBFILE_TYPE:
                case EXIF_OECF:
                case EXIF_ORIENTATION:
                case EXIF_PHOTOMETRIC_INTERPRETATION:
                case EXIF_PIXEL_X_DIMENSION:
                case EXIF_PIXEL_Y_DIMENSION:
                case EXIF_PLANAR_CONFIGURATION:
                case EXIF_PREDICTOR:
                case EXIF_PRIMARY_CHROMATICITIES:
                case EXIF_REFERENCE_BLACK_WHITE:
                case EXIF_RESOLUTION_UNIT:
                case EXIF_ROWS_PER_STRIP:
                case EXIF_SAMPLES_PER_PIXEL:
                case EXIF_STRIP_BYTE_COUNTS:
                case EXIF_STRIP_OFFSETS:
                case EXIF_SUBFILE_TYPE:
                case EXIF_SUB_IFDS:
                case EXIF_TILE_BYTE_COUNTS:
                case EXIF_TILE_LENGTH:
                case EXIF_TILE_OFFSETS:
                case EXIF_TRANSFER_FUNCTION:
                case EXIF_X_RESOLUTION:
                case EXIF_YCBCR_COEFFICIENTS:
                case EXIF_YCBCR_POSITIONING:
                case EXIF_YCBCR_SUBSAMPLING:
                case EXIF_Y_RESOLUTION:
                    continue;
                default:
                    final ImageMetaValue toValue = toDir.getValue( tagID );
                    if ( toValue == null )
                        toDir.putValue( tagID, me.getValue() );
            }
        }
    }

    /**
     * Creates a new {@link ImageMetadataDirectory} of the given class and
     * stores a local mapping to it by the given name.
     *
     * @param dirClass The class of the {@link ImageMetadataDirectory} class to
     * create.
     * @param name The name for the {@link ImageMetadataDirectory}.
     * @return Returns a new {@link ImageMetadataDirectory} for the given
     * {@link Class}.
     */
    private ImageMetadataDirectory createDirectoryFor(
        Class<? extends ImageMetadataDirectory> dirClass, String name )
    {
        try {
            final ImageMetadataDirectory dir = dirClass.newInstance();
            dir.setOwningMetadata( m_metadata );
            m_dirMap.put( name + m_ifdIndex, dir );
            return dir;
        }
        catch ( Exception e ) {
            throw new IllegalStateException( e );
        }
    }

    /**
     * Merge the values of the EXIF directories together.
     */
    private void mergeEXIFDirectories() {
        final ImageMetadataDirectory ifd0 = m_dirMap.get( "IFD0" );
        if ( ifd0 == null )
            return;
        for ( Map.Entry<String,ImageMetadataDirectory>
              me : m_dirMap.entrySet() ) {
            final ImageMetadataDirectory dir = me.getValue();
            if ( !(dir instanceof EXIFDirectory) || dir == ifd0 )
                continue;
            copyValuesFromTo( dir, ifd0 );
        }

        m_metadata.putDirectory( ifd0 );
    }

    /**
     * Read a camera manufacturer's maker notes.
     *
     * @param offset The offset from the beginning of the buffer of the maker
     * @param byteCount The total number of bytes for the maker notes data.
     */
    public void readMakerNotes( int offset, int byteCount,
                                ImageMetadataDirectory dir )
        throws IOException
    {
        int valueOffsetAdjustment = 0;
        final int[] adjustments = dir.getMakerNotesAdjustments( m_buf, offset );
        if ( adjustments != null ) {
            offset += adjustments[0];
            valueOffsetAdjustment = adjustments[1];
        }
        if ( !dir.readMakerNotes( m_buf, offset, byteCount ) ) {
            final ByteOrder origOrder = m_buf.probeOrder( offset );
            m_exifParser.parseDirectory( offset, valueOffsetAdjustment, dir );
            m_buf.order( origOrder );
        }
    }

    /**
     * A map of directory names (e.g., "IFD0", "SubIFD1") to directories.
     */
    private final Map<String,ImageMetadataDirectory> m_dirMap =
        new HashMap<String,ImageMetadataDirectory>();

    /**
     * A sequential index used to form the names of the IFDs.
     */
    private int m_ifdIndex = -1;
}
/* vim:set et sw=4 ts=4: */
