/*
 * #%L
 * BSD implementations of Bio-Formats readers and writers
 * %%
 * Copyright (C) 2005 - 2017 Open Microscopy Environment:
 *   - Board of Regents of the University of Wisconsin-Madison
 *   - Glencoe Software, Inc.
 *   - University of Dundee
 * %%
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * #L%
 */

package loci.formats.tiff;

import java.io.IOException;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.HashMap;

import loci.common.DebugTools;
import loci.formats.FormatException;
import loci.formats.FormatTools;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Data structure for working with TIFF Image File Directories (IFDs).
 *
 * @author Curtis Rueden ctrueden at wisc.edu
 * @author Eric Kjellman egkjellman at wisc.edu
 * @author Melissa Linkert melissa at glencoesoftware.com
 * @author Chris Allan callan at blackcat.ca
 */
public class IFD extends HashMap<Integer, Object> {

  // -- Constants --

  private static final Logger LOGGER = LoggerFactory.getLogger(IFD.class);

  // non-IFD tags (for internal use)
  public static final int LITTLE_ENDIAN = 0;
  public static final int BIG_TIFF = 1;
  public static final int REUSE = 3;

  // IFD tags
  public static final int NEW_SUBFILE_TYPE = 254;
  public static final int SUBFILE_TYPE = 255;
  public static final int IMAGE_WIDTH = 256;
  public static final int IMAGE_LENGTH = 257;
  public static final int BITS_PER_SAMPLE = 258;
  public static final int COMPRESSION = 259;
  public static final int PHOTOMETRIC_INTERPRETATION = 262;
  public static final int THRESHHOLDING = 263;
  public static final int CELL_WIDTH = 264;
  public static final int CELL_LENGTH = 265;
  public static final int FILL_ORDER = 266;
  public static final int DOCUMENT_NAME = 269;
  public static final int IMAGE_DESCRIPTION = 270;
  public static final int MAKE = 271;
  public static final int MODEL = 272;
  public static final int STRIP_OFFSETS = 273;
  public static final int ORIENTATION = 274;
  public static final int SAMPLES_PER_PIXEL = 277;
  public static final int ROWS_PER_STRIP = 278;
  public static final int STRIP_BYTE_COUNTS = 279;
  public static final int MIN_SAMPLE_VALUE = 280;
  public static final int MAX_SAMPLE_VALUE = 281;
  public static final int X_RESOLUTION = 282;
  public static final int Y_RESOLUTION = 283;
  public static final int PLANAR_CONFIGURATION = 284;
  public static final int PAGE_NAME = 285;
  public static final int X_POSITION = 286;
  public static final int Y_POSITION = 287;
  public static final int FREE_OFFSETS = 288;
  public static final int FREE_BYTE_COUNTS = 289;
  public static final int GRAY_RESPONSE_UNIT = 290;
  public static final int GRAY_RESPONSE_CURVE = 291;
  public static final int T4_OPTIONS = 292;
  public static final int T6_OPTIONS = 293;
  public static final int RESOLUTION_UNIT = 296;
  public static final int PAGE_NUMBER = 297;
  public static final int TRANSFER_FUNCTION = 301;
  public static final int SOFTWARE = 305;
  public static final int DATE_TIME = 306;
  public static final int ARTIST = 315;
  public static final int HOST_COMPUTER = 316;
  public static final int PREDICTOR = 317;
  public static final int WHITE_POINT = 318;
  public static final int PRIMARY_CHROMATICITIES = 319;
  public static final int COLOR_MAP = 320;
  public static final int HALFTONE_HINTS = 321;
  public static final int TILE_WIDTH = 322;
  public static final int TILE_LENGTH = 323;
  public static final int TILE_OFFSETS = 324;
  public static final int TILE_BYTE_COUNTS = 325;
  public static final int SUB_IFD = 330;
  public static final int INK_SET = 332;
  public static final int INK_NAMES = 333;
  public static final int NUMBER_OF_INKS = 334;
  public static final int DOT_RANGE = 336;
  public static final int TARGET_PRINTER = 337;
  public static final int EXTRA_SAMPLES = 338;
  public static final int SAMPLE_FORMAT = 339;
  public static final int S_MIN_SAMPLE_VALUE = 340;
  public static final int S_MAX_SAMPLE_VALUE = 341;
  public static final int TRANSFER_RANGE = 342;
  public static final int JPEG_TABLES = 347;
  public static final int JPEG_PROC = 512;
  public static final int JPEG_INTERCHANGE_FORMAT = 513;
  public static final int JPEG_INTERCHANGE_FORMAT_LENGTH = 514;
  public static final int JPEG_RESTART_INTERVAL = 515;
  public static final int JPEG_LOSSLESS_PREDICTORS = 517;
  public static final int JPEG_POINT_TRANSFORMS = 518;
  public static final int JPEG_Q_TABLES = 519;
  public static final int JPEG_DC_TABLES = 520;
  public static final int JPEG_AC_TABLES = 521;
  public static final int Y_CB_CR_COEFFICIENTS = 529;
  public static final int Y_CB_CR_SUB_SAMPLING = 530;
  public static final int Y_CB_CR_POSITIONING = 531;
  public static final int REFERENCE_BLACK_WHITE = 532;
  public static final int COPYRIGHT = 33432;
  public static final int EXIF = 34665;

  /** EXIF tags. */
  public static final int EXPOSURE_TIME = 33434;
  public static final int F_NUMBER = 33437;
  public static final int EXPOSURE_PROGRAM = 34850;
  public static final int SPECTRAL_SENSITIVITY = 34852;
  public static final int ISO_SPEED_RATINGS = 34855;
  public static final int OECF = 34856;
  public static final int EXIF_VERSION = 36864;
  public static final int DATE_TIME_ORIGINAL = 36867;
  public static final int DATE_TIME_DIGITIZED = 36868;
  public static final int COMPONENTS_CONFIGURATION = 37121;
  public static final int COMPRESSED_BITS_PER_PIXEL = 37122;
  public static final int SHUTTER_SPEED_VALUE = 37377;
  public static final int APERTURE_VALUE = 37378;
  public static final int BRIGHTNESS_VALUE = 37379;
  public static final int EXPOSURE_BIAS_VALUE = 37380;
  public static final int MAX_APERTURE_VALUE = 37381;
  public static final int SUBJECT_DISTANCE = 37382;
  public static final int METERING_MODE = 37383;
  public static final int LIGHT_SOURCE = 37384;
  public static final int FLASH = 37385;
  public static final int FOCAL_LENGTH = 37386;
  public static final int MAKER_NOTE = 37500;
  public static final int USER_COMMENT = 37510;
  public static final int SUB_SEC_TIME = 37520;
  public static final int SUB_SEC_TIME_ORIGINAL = 37521;
  public static final int SUB_SEC_TIME_DIGITIZED = 37522;
  public static final int FLASH_PIX_VERSION = 40960;
  public static final int COLOR_SPACE = 40961;
  public static final int PIXEL_X_DIMENSION = 40962;
  public static final int PIXEL_Y_DIMENSION = 40963;
  public static final int RELATED_SOUND_FILE = 40964;
  public static final int FLASH_ENERGY = 41483;
  public static final int SPATIAL_FREQUENCY_RESPONSE = 41484;
  public static final int FOCAL_PLANE_X_RESOLUTION = 41486;
  public static final int FOCAL_PLANE_Y_RESOLUTION = 41487;
  public static final int FOCAL_PLANE_RESOLUTION_UNIT = 41488;
  public static final int SUBJECT_LOCATION = 41492;
  public static final int EXPOSURE_INDEX = 41493;
  public static final int SENSING_METHOD = 41495;
  public static final int FILE_SOURCE = 41728;
  public static final int SCENE_TYPE = 41729;
  public static final int CFA_PATTERN = 41730;
  public static final int CUSTOM_RENDERED = 41985;
  public static final int EXPOSURE_MODE = 41986;
  public static final int WHITE_BALANCE = 41987;
  public static final int DIGITAL_ZOOM_RATIO = 41988;
  public static final int FOCAL_LENGTH_35MM_FILM = 41989;
  public static final int SCENE_CAPTURE_TYPE = 41990;
  public static final int GAIN_CONTROL = 41991;
  public static final int CONTRAST = 41992;
  public static final int SATURATION = 41993;
  public static final int SHARPNESS = 41994;
  public static final int SUBJECT_DISTANCE_RANGE = 41996;

  // -- Constructors --

  public IFD() {
    super();
  }

  public IFD(IFD ifd) {
    super(ifd);
  }

  // -- Tag retrieval methods --

  /** Gets whether this is a BigTIFF IFD. */
  public boolean isBigTiff() throws FormatException {
    return ((Boolean) getIFDValue(BIG_TIFF, Boolean.class)).booleanValue();
  }

  /** Gets whether the TIFF information in this IFD is little-endian. */
  public boolean isLittleEndian() throws FormatException {
    return ((Boolean) getIFDValue(LITTLE_ENDIAN, Boolean.class)).booleanValue();
  }

  /** Gets the given directory entry value from this IFD. */
  public Object getIFDValue(int tag) {
    return get(Integer.valueOf(tag));
  }

  /**
   * Gets the given directory entry value from this IFD,
   * performing some error checking.
   */
  public Object getIFDValue(int tag, Class checkClass) throws FormatException {
    Object value = get(Integer.valueOf(tag));
    if (checkClass != null && value != null && !checkClass.isInstance(value)) {
      // wrap object in array of length 1, if appropriate
      Class cType = checkClass.getComponentType();
      Object array =
        Array.newInstance(cType == null ? value.getClass() : cType, 1);
      if (cType == value.getClass()) {
        Array.set(array, 0, value);
      }
      else if (cType == boolean.class && value instanceof Boolean) {
        Array.setBoolean(array, 0, ((Boolean) value).booleanValue());
      }
      else if (cType == byte.class && value instanceof Byte) {
        Array.setByte(array, 0, ((Byte) value).byteValue());
      }
      else if (cType == char.class && value instanceof Character) {
        Array.setChar(array, 0, ((Character) value).charValue());
      }
      else if (cType == double.class && value instanceof Double) {
        Array.setDouble(array, 0, ((Double) value).doubleValue());
      }
      else if (cType == float.class && value instanceof Float) {
        Array.setFloat(array, 0, ((Float) value).floatValue());
      }
      else if (cType == int.class && value instanceof Integer) {
        Array.setInt(array, 0, ((Integer) value).intValue());
      }
      else if (cType == long.class && value instanceof Long) {
        Array.setLong(array, 0, ((Long) value).longValue());
      }
      else if (cType == short.class && value instanceof Short) {
        Array.setShort(array, 0, ((Short) value).shortValue());
      }
      else {
        try {
          if (checkClass.equals(String.class)) {
            StringBuilder sb = new StringBuilder();
            int l = Array.getLength(value);
            for (int i = 0; i < l; i++) {
              sb.append(Array.get(value, i));
              if (i < l - 1)
                sb.append(" ");
            }
            return sb.toString();
          } else {
            value = Array.get(value, 0);
            if (checkClass.isInstance(value))
              return value;
          }
        }
        catch (IllegalArgumentException exc) { }
        catch (ArrayIndexOutOfBoundsException exc) {
          // some files misbehave and report an array size of 1
          // when it is actually 0
          return null;
        }

        throw new FormatException(getIFDTagName(tag) +
          " directory entry is the wrong type (got " +
          value.getClass().getName() + ", expected " + checkClass.getName());
      }

      return array;
    }
    return value;
  }

  /**
   * Gets the given directory entry value in long format from this IFD,
   * performing some error checking.
   */
  public long getIFDLongValue(int tag, long defaultValue) throws FormatException
  {
    long value = defaultValue;
    Number number = (Number) getIFDValue(tag, Number.class);
    if (number != null) value = number.longValue();
    return value;
  }

  /**
   * Gets the given directory entry value in int format from this IFD,
   * or -1 if the given directory does not exist.
   */
  public int getIFDIntValue(int tag) {
    int value = -1;
    try {
      value = getIFDIntValue(tag,  -1);
    }
    catch (FormatException exc) { }
    return value;
  }

  /**
   * Gets the given directory entry value in int format from this IFD,
   * performing some error checking.
   */
  public int getIFDIntValue(int tag, int defaultValue) throws FormatException {
    int value = defaultValue;
    Number number = (Number) getIFDValue(tag, Number.class);
    if (number != null) value = number.intValue();
    return value;
  }

  /**
   * Gets the given directory entry value in rational format from this IFD,
   * performing some error checking.
   */
  public TiffRational getIFDRationalValue(int tag) throws FormatException {
    return (TiffRational) getIFDValue(tag, TiffRational.class);
  }

  /**
   * Gets the given directory entry value as a string from this IFD,
   * performing some error checking.
   */
  public String getIFDStringValue(int tag) throws FormatException {
    return (String) getIFDValue(tag, String.class);
  }

  /** Gets the given directory entry value as a string (regardless of type). */
  public String getIFDTextValue(int tag) {
    String value = null;
    Object o = getIFDValue(tag);
    if (o instanceof String[]) {
      StringBuilder sb = new StringBuilder();
      String[] s = (String[]) o;
      for (int i=0; i<s.length; i++) {
        sb.append(s[i]);
        if (i < s.length - 1) sb.append("\n");
      }
      value = sb.toString();
    }
    else if (o instanceof short[]) {
      final StringBuilder sb = new StringBuilder();
      for (short s : ((short[]) o)) {
        if (!Character.isISOControl((char) s)) {
          sb.append((char) s);
        }
        else if (s != 0) sb.append("\n");
      }
      value = sb.toString();
    }
    else if (o != null) value = o.toString();

    // sanitize line feeds
    if (value != null) {
      value = value.replace("\r\n", "\n"); // CR-LF to LF
      value = value.replace('\r', '\n'); // CR to LF
    }

    return value;
  }

  /**
   * Gets the given directory entry values in long format
   * from this IFD, performing some error checking.
   */
  public long[] getIFDLongArray(int tag) throws FormatException {
    Object value = getIFDValue(tag);
    long[] results = null;
    if (value instanceof long[]) results = (long[]) value;
    else if (value instanceof Number) {
      results = new long[] {((Number) value).longValue()};
    }
    else if (value instanceof Number[]) {
      Number[] numbers = (Number[]) value;
      results = new long[numbers.length];
      for (int i=0; i<results.length; i++) results[i] = numbers[i].longValue();
    }
    else if (value instanceof int[]) { // convert int[] to long[]
      int[] integers = (int[]) value;
      results = new long[integers.length];
      for (int i=0; i<integers.length; i++) results[i] = integers[i];
    }
    else if (value instanceof OnDemandLongArray) {
      try {
        results = ((OnDemandLongArray) value).toArray();
      }
      catch (IOException e) {
        throw new FormatException(e);
      }
    }
    else if (value != null) {
      throw new FormatException(getIFDTagName(tag) +
        " directory entry is the wrong type (got " +
        value.getClass().getName() +
        ", expected Number, long[], Number[] or int[])");
    }
    return results;
  }

  /**
   * Gets the given directory entry values in int format
   * from this IFD, performing some error checking.
   */
  public int[] getIFDIntArray(int tag) throws FormatException {
    Object value = getIFDValue(tag);
    int[] results = null;
    if (value instanceof int[]) results = (int[]) value;
    else if (value instanceof long[]) {
      long[] v = (long[]) value;
      results = new int[v.length];
      for (int i=0; i<v.length; i++) {
        results[i] = (int) v[i];
      }
    }
    else if (value instanceof Number) {
      results = new int[] {((Number) value).intValue()};
    }
    else if (value instanceof Number[]) {
      Number[] numbers = (Number[]) value;
      results = new int[numbers.length];
      for (int i=0; i<results.length; i++) results[i] = numbers[i].intValue();
    }
    else if (value != null) {
      throw new FormatException(getIFDTagName(tag) +
        " directory entry is the wrong type (got " +
        value.getClass().getName() + ", expected Number, int[] or Number[])");
    }
    return results;
  }

  /**
   * Gets the given directory entry values in short format
   * from this IFD, performing some error checking.
   */
  public short[] getIFDShortArray(int tag) throws FormatException {
    Object value = getIFDValue(tag);
    short[] results = null;
    if (value instanceof short[]) results = (short[]) value;
    else if (value instanceof int[]) {
      int[] v = (int[]) value;
      results = new short[v.length];
      for (int i=0; i<v.length; i++) {
        results[i] = (short) v[i];
      }
    }
    else if (value instanceof Number) {
      results = new short[] {((Number) value).shortValue()};
    }
    else if (value instanceof Number[]) {
      Number[] numbers = (Number[]) value;
      results = new short[numbers.length];
      for (int i=0; i<results.length; i++) {
        results[i] = numbers[i].shortValue();
      }
    }
    else if (value != null) {
      throw new FormatException(getIFDTagName(tag) +
        " directory entry is the wrong type (got " +
        value.getClass().getName() +
        ", expected Number, short[] or Number[])");
    }
    return results;
  }

  /** Convenience method for obtaining the ImageDescription from this IFD. */
  public String getComment() {
    return getIFDTextValue(IMAGE_DESCRIPTION);
  }

  /** Returns the width of an image tile. */
  public long getTileWidth() throws FormatException {
    long tileWidth = getIFDLongValue(TILE_WIDTH, 0);
    return tileWidth == 0 ? getImageWidth() : tileWidth;
  }

  /** Returns the length of an image tile. */
  public long getTileLength() throws FormatException {
    long tileLength = getIFDLongValue(TILE_LENGTH, 0);
    return tileLength == 0 ? getRowsPerStrip()[0] : tileLength;
  }

  /** Returns the number of image tiles per row. */
  public long getTilesPerRow() throws FormatException {
    long tileWidth = getTileWidth();
    long imageWidth = getImageWidth();
    long nTiles = imageWidth / tileWidth;
    if (nTiles * tileWidth < imageWidth) nTiles++;
    return nTiles;
  }

  /** Returns the number of image tiles per column. */
  public long getTilesPerColumn()
    throws FormatException
  {
    long tileLength = getTileLength();
    long imageLength = getImageLength();
    long nTiles = imageLength / tileLength;
    if (nTiles * tileLength < imageLength) nTiles++;
    return nTiles;
  }

  public boolean isTiled() throws FormatException {
    Object offsets = get(Integer.valueOf(STRIP_OFFSETS));
    Object tileWidth = get(Integer.valueOf(TILE_WIDTH));
    return offsets == null && tileWidth != null;
  }

  /**
   * Retrieves the image's width (TIFF tag ImageWidth) from a given TIFF IFD.
   * @return the image's width.
   * @throws FormatException if there is a problem parsing the IFD metadata.
   */
  public long getImageWidth() throws FormatException {
    long width = getIFDLongValue(IMAGE_WIDTH, 0);
    if (width > Integer.MAX_VALUE) {
      throw new FormatException("Sorry, ImageWidth > " + Integer.MAX_VALUE +
        " is not supported.");
    }
    return width;
  }

  /**
   * Retrieves the image's length (TIFF tag ImageLength) from a given TIFF IFD.
   * @return the image's length.
   * @throws FormatException if there is a problem parsing the IFD metadata.
   */
  public long getImageLength() throws FormatException {
    long length = getIFDLongValue(IMAGE_LENGTH, 0);
    if (length > Integer.MAX_VALUE) {
      throw new FormatException("Sorry, ImageLength > " + Integer.MAX_VALUE +
        " is not supported.");
    }
    return length;
  }

  /**
   * Retrieves the image's bits per sample (TIFF tag BitsPerSample) from a given
   * TIFF IFD.
   * @return the image's bits per sample. The length of the array is equal to
   *   the number of samples per pixel.
   * @throws FormatException if there is a problem parsing the IFD metadata.
   * @see #getSamplesPerPixel()
   */
  public int[] getBitsPerSample() throws FormatException {
    int[] bitsPerSample = getIFDIntArray(BITS_PER_SAMPLE);
    if (bitsPerSample == null) bitsPerSample = new int[] {1};

    int samplesPerPixel = getSamplesPerPixel();
    if (bitsPerSample.length < samplesPerPixel) {
      LOGGER.debug("BitsPerSample length ({}) does not match " +
        "SamplesPerPixel ({})", bitsPerSample.length, samplesPerPixel);
      int bits = bitsPerSample[0];
      bitsPerSample = new int[samplesPerPixel];
      Arrays.fill(bitsPerSample, bits);
    }
    int nSamples = (int) Math.min(bitsPerSample.length, samplesPerPixel);
    for (int i=0; i<nSamples; i++) {
      if (bitsPerSample[i] < 1) {
        throw new FormatException("Illegal BitsPerSample (" +
          bitsPerSample[i] + ")");
      }
    }

    return bitsPerSample;
  }

  /**
   * Retrieves the image's pixel type based on the BitsPerSample tag.
   * @return the pixel type.  This is one of:
   *  <li>FormatTools.INT8</li>
   *  <li>FormatTools.UINT8</li>
   *  <li>FormatTools.INT16</li>
   *  <li>FormatTools.UINT16</li>
   *  <li>FormatTools.INT32</li>
   *  <li>FormatTools.UINT32</li>
   *  <li>FormatTools.FLOAT</li>
   *  <li>FormatTools.DOUBLE</li>
   * @throws FormatException if there is a problem parsing the IFD metadata.
   * @see #getBitsPerSample()
   */
  public int getPixelType() throws FormatException {
    int bps = getBitsPerSample()[0];
    int bitFormat = getIFDIntValue(SAMPLE_FORMAT);

    while (bps % 8 != 0) bps++;
    if (bps == 24 && bitFormat != 3) bps = 32;

    switch (bps) {
      case 16:
        if (bitFormat == 3) return FormatTools.FLOAT;
        return bitFormat == 2 ? FormatTools.INT16 : FormatTools.UINT16;
      case 24:
        return FormatTools.FLOAT;
      case 64:
        if (bitFormat != 3) {
          throw new FormatException("64-bit int data not supported");
        }
        return FormatTools.DOUBLE;
      case 32:
        if (bitFormat == 3) return FormatTools.FLOAT;
        return bitFormat == 2 ? FormatTools.INT32 : FormatTools.UINT32;
      default:
        return bitFormat == 2 ? FormatTools.INT8 : FormatTools.UINT8;
    }
  }

  /**
   * Retrieves the image's bytes per sample (derived from tag BitsPerSample)
   * from this IFD.
   * @return the image's bytes per sample.  The length of the array is equal to
   *   the number of samples per pixel.
   * @throws FormatException if there is a problem parsing the IFD metadata.
   * @see #getSamplesPerPixel()
   * @see #getBitsPerSample()
   */
  public int[] getBytesPerSample() throws FormatException {
    int[] bitsPerSample = getBitsPerSample();
    int[] bps = new int[bitsPerSample.length];
    for (int i=0; i<bitsPerSample.length; i++) {
      bps[i] = bitsPerSample[i];
      while ((bps[i] % 8) != 0) bps[i]++;
      bps[i] /= 8;
      if (bps[i] == 0) bps[i] = 1;
    }
    return bps;
  }

  /**
   * Retrieves the number of samples per pixel for the image (TIFF tag
   * SamplesPerPixel) from this IFD.
   * @return the number of samples per pixel.
   * @throws FormatException if there is a problem parsing the IFD metadata.
   */
  public int getSamplesPerPixel() throws FormatException {
    if (getCompression() == TiffCompression.OLD_JPEG) {
      return 3; // always RGB
    }
    return getIFDIntValue(SAMPLES_PER_PIXEL, 1);
  }

  /**
   * Retrieves the image's compression type (TIFF tag Compression) from
   * this IFD.
   *
   * @return the image's compression type. As of TIFF 6.0 this is one of:
   * <ul>
   *  <li>Uncompressed (1)</li>
   *  <li>CCITT 1D (2)</li>
   *  <li>Group 3 Fax (3)</li>
   *  <li>Group 4 Fax (4)</li>
   *  <li>LZW (5)</li>
   *  <li>JPEG (6)</li>
   *  <li>PackBits (32773)</li>
   * </ul>
   * Other proprietary compression types are also possible;
   * see {@link TiffCompression} for more details.
   *
   * @throws FormatException if there is a problem parsing the IFD metadata.
   */
  public TiffCompression getCompression() throws FormatException {
    return TiffCompression.get(getIFDIntValue(
        COMPRESSION, TiffCompression.UNCOMPRESSED.getCode()));
  }

  /**
   * Retrieves the image's photometric interpretation (TIFF tag
   * PhotometricInterpretation) from this IFD.
   * @return the image's photometric interpretation. As of TIFF 6.0 this is one
   * of:
   * <ul>
   *  <li>WhiteIsZero (0)</li>
   *  <li>BlackIsZero (1)</li>
   *  <li>RGB (2)</li>
   *  <li>RGB Palette (3)</li>
   *  <li>Transparency mask (4)</li>
   *  <li>CMYK (5)</li>
   *  <li>YbCbCr (6)</li>
   *  <li>CIELab (8)</li>
   * </ul>
   *
   * @throws FormatException if there is a problem parsing the IFD metadata.
   */
  public PhotoInterp getPhotometricInterpretation() throws FormatException {
    Object photo = getIFDValue(PHOTOMETRIC_INTERPRETATION);
    if (photo instanceof PhotoInterp) return (PhotoInterp) photo;
    if (photo == null && getCompression() == TiffCompression.OLD_JPEG) {
      return PhotoInterp.RGB;
    }
    int pi = photo instanceof Number ? ((Number) photo).intValue() :
      ((int[]) photo)[0];
    return PhotoInterp.get(pi);
  }

  /**
   * Retrieves the image's planar configuration (TIFF tag PlanarConfiguration)
   * from this IFD.
   * @return the image's planar configuration.  As of TIFF 6.0 this is one of:
   * <ul>
   *  <li>Chunky (1)</li>
   *  <li>Planar (2)</li>
   * </ul>
   *
   * @throws FormatException if there is a problem parsing the IFD metadata.
   */
  public int getPlanarConfiguration() throws FormatException {
    int planarConfig = getIFDIntValue(PLANAR_CONFIGURATION, 1);
    if (planarConfig != 1 && planarConfig != 2) {
      throw new FormatException("Sorry, PlanarConfiguration (" + planarConfig +
        ") not supported.");
    }
    return planarConfig;
  }

  /**
   * Retrieves the strip offsets for the image (TIFF tag StripOffsets) from
   * this IFD.
   * @return the strip offsets for the image. The length of the array is equal
   *   to the number of strips per image. <i>StripsPerImage =
   *   floor ((ImageLength + RowsPerStrip - 1) / RowsPerStrip)</i>.
   * @throws FormatException if there is a problem parsing the IFD metadata.
   * @see #getStripByteCounts()
   * @see #getRowsPerStrip()
   */
  public long[] getStripOffsets() throws FormatException {
    int tag = isTiled() ? TILE_OFFSETS : STRIP_OFFSETS;
    long[] offsets = null;
    OnDemandLongArray compressedOffsets = getOnDemandStripOffsets();
    if (compressedOffsets != null) {
      offsets = new long[(int) compressedOffsets.size()];
      try {
        for (int q=0; q<offsets.length; q++) {
          offsets[q] = compressedOffsets.get(q);
        }
      }
      catch (IOException e) {
        throw new FormatException("Failed to retrieve offset", e);
      }
    }
    else {
      offsets = getIFDLongArray(tag);
    }
    if (isTiled() && offsets == null) {
      offsets = getIFDLongArray(STRIP_OFFSETS);
    }
    if (offsets == null) return null;

    for (int i=0; i<offsets.length; i++) {
      if (offsets[i] < 0) {
        offsets[i] += 0x100000000L;
      }
    }

    if (isTiled()) return offsets;
    long rowsPerStrip = getRowsPerStrip()[0];
    long numStrips = (getImageLength() + rowsPerStrip - 1) / rowsPerStrip;
    if (getPlanarConfiguration() == 2) numStrips *= getSamplesPerPixel();
    if (offsets.length < numStrips) {
      throw new FormatException("StripOffsets length (" + offsets.length +
        ") does not match expected " + "number of strips (" + numStrips + ")");
    }
    return offsets;
  }

  public OnDemandLongArray getOnDemandStripOffsets() throws FormatException
  {
    int tag = isTiled() ? TILE_OFFSETS : STRIP_OFFSETS;
    Object offsets = getIFDValue(tag);
    if (offsets instanceof OnDemandLongArray) {
      return (OnDemandLongArray) offsets;
    }
    return null;
  }

  /**
   * Retrieves strip byte counts for the image (TIFF tag StripByteCounts) from
   * this IFD.
   * @return the byte counts for each strip. The length of the array is equal
   *   to the number of strips per image. <i>StripsPerImage =
   *   floor((ImageLength + RowsPerStrip - 1) / RowsPerStrip)</i>.
   * @throws FormatException if there is a problem parsing the IFD metadata.
   * @see #getStripOffsets()
   */
  public long[] getStripByteCounts() throws FormatException {
    int tag = isTiled() ? TILE_BYTE_COUNTS : STRIP_BYTE_COUNTS;
    long[] byteCounts = getIFDLongArray(tag);
    if (isTiled() && byteCounts == null) {
      byteCounts = getIFDLongArray(STRIP_BYTE_COUNTS);
    }
    long imageLength = getImageLength();
    if (byteCounts == null) {
      // technically speaking, this shouldn't happen (since TIFF writers are
      // required to write the StripByteCounts tag), but we'll support it
      // anyway

      // don't rely on RowsPerStrip, since it's likely that if the file doesn't
      // have the StripByteCounts tag, it also won't have the RowsPerStrip tag
      long[] offsets = getStripOffsets();
      if (offsets == null) return null;
      int bytesPerSample = getBytesPerSample()[0];
      long imageWidth = getImageWidth();
      byteCounts = new long[offsets.length];
      int samples = getSamplesPerPixel();
      long imageSize = imageWidth * imageLength * bytesPerSample *
        (getPlanarConfiguration() == 2 ? 1 : samples);
      long count = imageSize / byteCounts.length;
      Arrays.fill(byteCounts, count);
    }

    long[] counts = new long[byteCounts.length];

    if (getCompression() == TiffCompression.LZW &&
      (!containsKey(ROWS_PER_STRIP) ||
      ((imageLength % getRowsPerStrip()[0])) != 0))
    {
      for (int i=0; i<byteCounts.length; i++) {
        counts[i] = byteCounts[i] * 2;
      }
    }
    else System.arraycopy(byteCounts, 0, counts, 0, counts.length);

    if (isTiled()) return counts;

    long rowsPerStrip = getRowsPerStrip()[0];
    long numStrips = (getImageLength() + rowsPerStrip - 1) / rowsPerStrip;
    if (getPlanarConfiguration() == 2) numStrips *= getSamplesPerPixel();

    if (counts.length < numStrips) {
      throw new FormatException("StripByteCounts length (" + counts.length +
        ") does not match expected " + "number of strips (" + numStrips + ")");
    }

    return counts;
  }

  /**
   * Retrieves the number of rows per strip for image (TIFF tag RowsPerStrip)
   * from this IFD.
   * @return the number of rows per strip.
   * @throws FormatException if there is a problem parsing the IFD metadata.
   */
  public long[] getRowsPerStrip() throws FormatException {
    long[] rowsPerStrip = getIFDLongArray(ROWS_PER_STRIP);
    if (rowsPerStrip == null) {
      // create a fake RowsPerStrip entry if one is not present 
      long tileLength = getIFDLongValue(TILE_LENGTH, 0);
      return new long[] {tileLength == 0 ? getImageLength() : tileLength};
    }

    // rowsPerStrip should never be more than the total number of rows
    long imageLength = getImageLength();
    for (int i=0; i<rowsPerStrip.length; i++) {
      rowsPerStrip[i] = (long) Math.min(rowsPerStrip[i], imageLength);
    }

    long rows = rowsPerStrip[0];
    for (int i=1; i<rowsPerStrip.length; i++) {
      if (rows != rowsPerStrip[i]) {
        throw new FormatException(
          "Sorry, non-uniform RowsPerStrip is not supported");
      }
    }

    return rowsPerStrip;
  }

  /**
   * Retrieve the value by which to multiply the X and Y resolution so that
   * the resolutions are in microns per pixel.
   */
  public int getResolutionMultiplier() {
    int resolutionUnit = getIFDIntValue(RESOLUTION_UNIT);
    int multiplier = 1;
    if (resolutionUnit == 2) {
      multiplier = 25400;
    }
    else if (resolutionUnit == 3) {
      multiplier = 10000;
    }
    return multiplier;
  }

  /**
   * Retrieve the X resolution (TIFF tag XResolution) from this IFD.
   * The resolution will be normalized to microns per pixel.
   *
   * @return the X resolution, in microns per pixel
   * @throws FormatException if there is a problem parsing the IFD metadata.
   */
  public double getXResolution() throws FormatException {
    TiffRational xResolution = getIFDRationalValue(X_RESOLUTION);
    double x = xResolution == null ? 0 : 1 / xResolution.doubleValue();

    int multiplier = getResolutionMultiplier();
    return x * multiplier;
  }

  /**
   * Retrieve the Y resolution (TIFF tag YResolution) from this IFD.
   * The resolution will be normalized to microns per pixel.
   *
   * @return the Y resolution, in microns per pixel
   * @throws FormatException if there is a problem parsing the IFD metadata.
   */
  public double getYResolution() throws FormatException {
    TiffRational yResolution = getIFDRationalValue(Y_RESOLUTION);
    double y = yResolution == null ? 0 : 1 / yResolution.doubleValue();

    int resolutionUnit = getIFDIntValue(RESOLUTION_UNIT);
    int multiplier = getResolutionMultiplier();
    return y * multiplier;
  }

  // -- IFD population methods --

  /** Adds a directory entry to this IFD. */
  public void putIFDValue(int tag, Object value) {
    put(Integer.valueOf(tag), value);
  }

  /** Adds a directory entry of type BYTE to this IFD. */
  public void putIFDValue(int tag, short value) {
    putIFDValue(tag, Short.valueOf(value));
  }

  /** Adds a directory entry of type SHORT to this IFD. */
  public void putIFDValue(int tag, int value) {
    putIFDValue(tag, Integer.valueOf(value));
  }

  /** Adds a directory entry of type LONG to this IFD. */
  public void putIFDValue(int tag, long value) {
    putIFDValue(tag, Long.valueOf(value));
  }

  // -- Debugging --

  /** Prints the contents of this IFD. */
  public void printIFD() {
    LOGGER.trace("IFD directory entry values:");

    for (Integer tag : keySet()) {
      Object value = get(tag);
      String v = null;
      if (value == null) {
        LOGGER.trace("\t{}=null", getIFDTagName(tag.intValue()));
      }
      else if ((value instanceof Boolean) || (value instanceof Number) ||
        (value instanceof String) || (value instanceof PhotoInterp) ||
        (value instanceof TiffCompression) || (value instanceof TiffIFDEntry))
      {
        v = value.toString();
        LOGGER.trace("\t{}={}", getIFDTagName(tag.intValue()), v);
      }
      else if (value instanceof OnDemandLongArray) {
        // this is an array of primitive types, Strings, or TiffRationals
        LOGGER.trace("\t{}=", getIFDTagName(tag.intValue()));
        try {
          OnDemandLongArray dvalue = (OnDemandLongArray) value;
          long nElements = dvalue.size();
          long[] darray = dvalue.toArray();
          for (long i=0; i<nElements; i++) {
            LOGGER.trace("\t\t{}", Array.get(darray, (int) i));
          }
        }
        catch (IOException e) {
          LOGGER.trace("Error reading array", e);
        }
      }
      else {
        // this is an array of primitive types, Strings, or TiffRationals
        LOGGER.trace("\t{}=", getIFDTagName(tag.intValue()));
        int nElements = Array.getLength(value);
        for (int i=0; i<nElements; i++) {
          LOGGER.trace("\t\t{}", Array.get(value, i));
        }
      }
    }
  }

  // -- Utility methods --

  /** Gets the name of the IFD tag encoded by the given number. */
  public static String getIFDTagName(int tag) { return getFieldName(tag); }

  /**
   * This method uses reflection to scan the values of this class's
   * static fields, returning the first matching field's name. It is
   * probably not very efficient, and is mainly intended for debugging.
   */
  public static String getFieldName(int value) {
    return DebugTools.getFieldName(IFD.class, value);
  }

}
