/*
 * @(#)IFDEntry.java  2.0  2010-07-24
 * 
 * Copyright (c) 2009-2010 Werner Randelshofer, Immensee, Switzerland.
 * All rights reserved.
 *
 * You may not use, copy or modify this file, except in compliance with the
 * license agreement you entered into with Werner Randelshofer.
 * For details see accompanying license terms.
 */
package org.monte.media.tiff;

import java.io.IOException;
import java.nio.ByteOrder;

/**
 * Represents a directory entry in a TIFF Image File Directory (IFD).
 * <p>
 * Each 12-byte IFD entry has the following format:
 * <ul>
 * <li>Bytes 0-1 The Tag that identifies the field.</li>
 * <li>Bytes 2-3 The field Type.</li>
 * <li>Bytes 4-7 The number of values, Count of the indicated Type.</li>
 * <li>Bytes 8-11 The Value Offset, the file offset (in bytes) of the Value for the
 * field. The Value is expected to begin on a word boundary; the corresponding
 * Value Offset will thus be an even number. This file offset may point anywhere
 * in the file, even after the image data.</li>
 * </ul>
 * @author Werner Randelshofer
 * @version 2.1 2010-09-07 Stores ifdOffset.
 * <br>2.0 2010-07-24 Reworked.
 * <br>1.0 2009-12-26 Created.
 */
public class IFDEntry {

    /** The Tag number that identifies the field. */
    private int tagNumber;
    /** The field Type. */
    private int typeNumber;
    /** The number of values, Count of the indicated Type. */
    private long count;
    /** The Value Offset stores the value or the offset of the value depending
     * on typeNumber and on count. */
    private long valueOffset;
    /** The Entry Offset stores the location of the entry in the file. */
    private long entryOffset;
    /** The IFD Offset stores the location of the IFD in the file. */
    private long ifdOffset;
    /* The entry data. */
    private Object data;

    public IFDEntry(int tagNumber, int typeNumber, long count, long valueOffset, long entryOffset) {
        this.tagNumber = tagNumber;
        this.typeNumber = typeNumber;
        this.count = count;
        this.valueOffset = valueOffset;
        this.entryOffset = entryOffset;
    }

    public long getCount() {
        return count;
    }

    public int getTagNumber() {
        return tagNumber;
    }

    public int getTypeNumber() {
        return typeNumber;
    }

    /** The value offset may either contain the data or point to the data
     * depending on the type and the count.
     *
     * @return The value offset.
     */
    public long getValueOffset() {
        return valueOffset;
    }

    /** The offset to the data. */
    public long getDataOffset() {
        return isDataInValueOffset() ? entryOffset + 8 : valueOffset+ifdOffset;
    }

    public void setIFDOffset(long newValue) {
        ifdOffset = newValue;
    }

    public long getEntryOffset() {
        return entryOffset;
    }
    public long getIFDOffset() {
        return ifdOffset;
    }

    public boolean isDataInValueOffset() {
        switch (IFDDataType.valueOf(typeNumber)) {
            case ASCII://8-bit byte that contains a 7-bit ASCII code; the last byte
                //must be NUL (binary zero).
                return false;
            case BYTE://8-bit unsigned integer.
                return count <= 4;
            case SHORT://16-bit (2-byte) unsigned integer.
                return count <= 2;
            case LONG://32-bit (4-byte) unsigned integer.
                return count <= 1;
            case RATIONAL://Two LONGs: the first represents the numerator of a fraction; the second, the denominator.
                return false;
            case SBYTE: //An 8-bit signed (twos-complement) integer.
                return count <= 4;
            case UNDEFINED://An 8-bit byte that may contain anything, depending on the definition of the field.
                return count <= 4;
            case SSHORT://A 16-bit (2-byte) signed (twos-complement) integer.
                return count <= 2;
            case SLONG://A 32-bit (4-byte) signed (twos-complement) integer.
                return count <= 1;
            case SRATIONAL://Two SLONG’s: the first represents the numerator of a fraction, the second the denominator.
                return false;
            case FLOAT://Single precision (4-byte) IEEE prettyFormat.
                return count <= 1;
            case DOUBLE:// Double precision (8-byte) IEEE prettyFormat.
                return false;
            default:
                return true;
        }
    }

    public long getLength() {
        switch (IFDDataType.valueOf(typeNumber)) {
            case ASCII://8-bit byte that contains a 7-bit ASCII code; the last byte
                return count;
            case BYTE://8-bit unsigned integer.
                return count;
            case SHORT://16-bit (2-byte) unsigned integer.
                return count * 2;
            case LONG://32-bit (4-byte) unsigned integer.
                return count * 4;
            case RATIONAL://Two LONGs: the first represents the numerator of a fraction; the second, the denominator.
                return count * 8;
            case SBYTE: //An 8-bit signed (twos-complement) integer.
                return count;
            case UNDEFINED://An 8-bit byte that may contain anything, depending on the definition of the field.
                return count;
            case SSHORT://A 16-bit (2-byte) signed (twos-complement) integer.
                return count * 2;
            case SLONG://A 32-bit (4-byte) signed (twos-complement) integer.
                return count * 4;
            case SRATIONAL://Two SLONG’s: the first represents the numerator of a fraction, the second the denominator.
                return count * 8;
            case FLOAT://Single precision (4-byte) IEEE prettyFormat.
                return count * 4;
            case DOUBLE:// Double precision (8-byte) IEEE prettyFormat.
                return count * 8;
            default:
                return 0;
        }
    }

    /** Reads value data with ifdDataOffset=0*/
    public Object readData(TIFFInputStream in) throws IOException {
        return readData(in, ifdOffset);
    }

    /** Reads value data with the specified ifdDataOffset.*/
    public Object readData(TIFFInputStream in, long ifdDataOffset) throws IOException {
        Object d = null;
        IFDDataType tt = IFDDataType.valueOf(typeNumber);
        if (tt != null) {
            switch (tt) {
                case ASCII://8-bit byte that contains a 7-bit ASCII code; the last byte
                    //must be NUL (binary zero).
                    if (count <= 4) {
                        StringBuilder buf = new StringBuilder();
                        int data = (int) valueOffset;
                        if (in.getByteOrder() == ByteOrder.LITTLE_ENDIAN) {
                            for (int i = 0; i < count - 1; i++) {
                                buf.append((char) (data & 0xff));
                                data >>= 8;
                            }
                        } else {
                            for (int i = 0; i < count - 1; i++) {
                                buf.append((char) (data >>> 24));
                                data <<= 8;
                            }
                        }
                        return buf.toString();
                    } else {
                        return in.readASCII(valueOffset + ifdDataOffset, count);
                    }
                case SHORT://16-bit (2-byte) unsigned integer.
                    if (count == 1) {
                        if (in.getByteOrder() == ByteOrder.LITTLE_ENDIAN) {
                            d = (int) (valueOffset & 0xffff);
                        } else {
                            d = (int) ((valueOffset >> 16) & 0xffff);
                        }
                    } else if (count == 2) {
                        d = new int[]{(int) (valueOffset & 0xffff), (int) ((valueOffset & 0xffff0000) >> 16)};
                    } else {
                        d = in.readSHORT(valueOffset + ifdDataOffset, count);
                    }
                    break;
                case LONG://32-bit (4-byte) unsigned integer.
                    if (count == 1) {
                        d = valueOffset;
                    } else {
                        d = in.readLONG(valueOffset + ifdDataOffset, count);
                    }
                    break;
                case RATIONAL://Two LONGs: the first represents the numerator of a fraction; the second, the denominator.
                    if (count == 1) {
                        d = in.readRATIONAL(valueOffset + ifdDataOffset);
                    } else {
                        d = in.readRATIONAL(valueOffset + ifdDataOffset, count);
                    }
                    break;
                case BYTE://8-bit unsigned integer.
                    if (count == 1) {
                        d = (short) (valueOffset & 0xff);
                    } else if (count == 2) {
                        d = new short[]{(short) ((valueOffset & 0xff00) >> 8), (short) (valueOffset & 0xff)};
                    } else if (count == 3) {
                        d = new short[]{(short) ((valueOffset & 0xff0000) >> 16), (short) ((valueOffset & 0xff00) >> 8), (short) (valueOffset & 0xff)};
                    } else if (count == 4) {
                        d = new short[]{(short) ((valueOffset & 0xff000000) >> 24), (short) ((valueOffset & 0xff0000) >> 16), (short) ((valueOffset & 0xff00) >> 8), (short) (valueOffset & 0xff)};
                    } else {
                        byte[] b = new byte[(int) count];
                        in.read(valueOffset + ifdDataOffset, b, 0, b.length);
                        short[] s = new short[(int) count];
                        for (int i = 0; i < b.length; i++) {
                            s[i] = (short) (b[i] & 0xff);
                        }
                        d = s;
                    }
                    break;
                case SBYTE: //An 8-bit signed (twos-complement) integer.
                case UNDEFINED://An 8-bit byte that may contain anything, depending on the definition of the field.
                    if (count == 1) {
                        d = (byte) valueOffset;
                    } else if (count == 2) {
                        d = new byte[]{(byte) ((valueOffset & 0xff00) >> 8), (byte) (valueOffset & 0xff)};
                    } else if (count == 3) {
                        d = new byte[]{(byte) ((valueOffset & 0xff0000) >> 16), (byte) ((valueOffset & 0xff00) >> 8), (byte) (valueOffset & 0xff)};
                    } else if (count == 4) {
                        d = new byte[]{(byte) ((valueOffset & 0xff000000) >> 24), (byte) ((valueOffset & 0xff0000) >> 16), (byte) ((valueOffset & 0xff00) >> 8), (byte) (valueOffset & 0xff)};
                    } else {
                        byte[] b = new byte[(int) count];
                        in.read(valueOffset + ifdDataOffset, b, 0, b.length);
                        d = b;
                    }
                    break;
                case SSHORT://A 16-bit (2-byte) signed (twos-complement) integer.
                    if (count == 1) {
                        if (in.getByteOrder() == ByteOrder.LITTLE_ENDIAN) {
                            d = (short) (valueOffset & 0xffff);
                        } else {
                            d = (short) ((valueOffset >> 16) & 0xffff);
                        }
                    } else if (count == 2) {
                        d = new int[]{(short) (valueOffset & 0xffff), (short) ((valueOffset & 0xffff0000) >> 16)};
                    } else {
                        d = in.readSSHORT(valueOffset + ifdDataOffset, count);
                    }
                    break;
                case SLONG://A 32-bit (4-byte) signed (twos-complement) integer.
                    throw new IOException("Format " + typeNumber + " not implemented");
                case SRATIONAL://Two SLONG’s: the first represents the numerator of a fraction, the second the denominator.
                    if (count == 1) {
                        d = in.readSRATIONAL(valueOffset + ifdDataOffset);
                    } else {
                        d = in.readSRATIONAL(valueOffset + ifdDataOffset, count);
                    }
                    break;
                case FLOAT://Single precision (4-byte) IEEE prettyFormat.
                case DOUBLE:// Double precision (8-byte) IEEE prettyFormat.
                default:
                    throw new IOException("Format " + typeNumber + " not implemented");
            }
        }
        return d;
    }

    public void loadData(TIFFInputStream in) throws IOException {
        data = readData(in);
    }

    public Object getData() {
        return data;
    }

    /** FIXME Output is used by EXIFView */
    @Override
    public String toString() {
        return "IFD Entry: tag:0x" + Integer.toHexString(tagNumber) + " type:0x" + Integer.toHexString(typeNumber) + " count:0x" + Long.toHexString(count) + " valueOffset:0x" + Long.toHexString(valueOffset);
    }

    public String toString(Enum tagName) {
        StringBuilder buf = new StringBuilder();
        buf.append(
                "Entry tag:" + tagName + "(" + Integer.toHexString(tagNumber) + "), type:" + IFDDataType.valueOf(typeNumber) + "(" + typeNumber + "), count:" + count + ", valueOffset:" + valueOffset);
        if (data != null) {
            buf.append(", data:");
            if (data instanceof byte[]) {
                byte[] d = (byte[]) data;
                for (int i = 0; i < d.length; i++) {
                    if (i != 0) {
                        buf.append(',');
                    }
                    buf.append(d[i]);
                }
            } else if (data instanceof short[]) {
                short[] d = (short[]) data;
                for (int i = 0; i < d.length; i++) {
                    if (i != 0) {
                        buf.append(',');
                    }
                    buf.append(d[i]);
                }
            } else if (data instanceof int[]) {
                int[] d = (int[]) data;
                for (int i = 0; i < d.length; i++) {
                    if (i != 0) {
                        buf.append(',');
                    }
                    buf.append(d[i]);
                }
            } else if (data instanceof long[]) {
                long[] d = (long[]) data;
                for (int i = 0; i < d.length; i++) {
                    if (i != 0) {
                        buf.append(',');
                    }
                    buf.append(d[i]);
                }
            } else if (data instanceof Object[]) {
                Object[] d = (Object[]) data;
                for (int i = 0; i < d.length; i++) {
                    if (i != 0) {
                        buf.append(',');
                    }
                    buf.append(d[i]);
                }
            } else {
                buf.append(data);
            }
        }
        return buf.toString();
    }
}
