/*
 * #%L
 * Top-level reader and writer APIs
 * %%
 * 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;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

import loci.common.Location;
import loci.common.RandomAccessInputStream;
import loci.formats.codec.Codec;
import loci.formats.codec.CodecOptions;
import loci.formats.in.MetadataLevel;
import loci.formats.in.MetadataOptions;
import loci.formats.in.DynamicMetadataOptions;
import loci.formats.meta.MetadataStore;

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

/**
 * ImageReader is the master file format reader for all supported formats.
 * It uses one instance of each reader subclass (specified in readers.txt,
 * or other class list source) to identify file formats and read data.
 *
 * @author Curtis Rueden ctrueden at wisc.edu
 */
public class ImageReader implements IFormatReader {

  // -- Constants --

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

  // -- Static fields --

  /** Default list of reader classes, for use with noargs constructor. */
  private static ClassList<IFormatReader> defaultClasses;

  // -- Static utility methods --

  public static ClassList<IFormatReader> getDefaultReaderClasses() {
    if (defaultClasses == null) {
      // load built-in reader classes from readers.txt file
      try {
        defaultClasses =
          new ClassList<IFormatReader>("readers.txt", IFormatReader.class);
      }
      catch (IOException exc) {
        defaultClasses = new ClassList<IFormatReader>(IFormatReader.class);
        LOGGER.info("Could not parse class list; using default classes", exc);
      }
    }
    return defaultClasses;
  }

  // -- Fields --

  /** List of supported file format readers. */
  private IFormatReader[] readers;

  /**
   * Valid suffixes for this file format.
   * Populated the first time getSuffixes() is called.
   */
  private String[] suffixes;

  /** Name of current file. */
  private String currentId;

  /** Current form index. */
  private int current;

  private boolean allowOpen = true;

  private boolean isOmero(String id) {
    return id != null && id.toLowerCase().startsWith("omero:") &&
    id.indexOf("\n") > 0;
  }

  // -- Constructors --

  /**
   * Constructs a new ImageReader with the default
   * list of reader classes from readers.txt.
   */
  public ImageReader() {
    this(getDefaultReaderClasses());
  }

  /** Constructs a new ImageReader from the given list of reader classes. */
  public ImageReader(ClassList<IFormatReader> classList) {
    // add readers to the list
    List<IFormatReader> list = new ArrayList<IFormatReader>();
    Class<? extends IFormatReader>[] c = classList.getClasses();
    // assign the same options instance to all readers
    MetadataOptions opt = new DynamicMetadataOptions();
    for (int i=0; i<c.length; i++) {
      IFormatReader reader = null;
      try {
        reader = c[i].getDeclaredConstructor().newInstance();
        reader.setMetadataOptions(opt);
      }
      catch (ReflectiveOperationException exc) { }
      if (reader == null) {
        LOGGER.error("{} cannot be instantiated.", c[i].getName());
        continue;
      }
      list.add(reader);
    }
    readers = new IFormatReader[list.size()];
    list.toArray(readers);
  }

  // -- ImageReader API methods --

  /**
   * Toggles whether or not file system access is allowed when doing type
   * detection.  By default, file system access is allowed.
   */
  public void setAllowOpenFiles(boolean allowOpen) {
    this.allowOpen = allowOpen;
  }

  /** Gets a string describing the file format for the given file. */
  public String getFormat(String id) throws FormatException, IOException {
    return getReader(id).getFormat();
  }

  /** Gets the reader used to open the given file. */
  public IFormatReader getReader(String id)
    throws FormatException, IOException
  {
   // HACK: skip file existence check for fake files
   if (id.endsWith(File.separator)) {
    id = id.substring(0, id.length() - 1);
   }
   boolean fake = id != null && id.toLowerCase().endsWith(".fake");
   boolean omero = isOmero(id);

   // block temporary files that are being copied e.g. by WinSCP
   boolean invalid = id != null && id.toLowerCase().endsWith(".filepart");

   // NB: Check that we can generate a valid handle for the ID;
   // e.g., for files, this will throw an exception if the file is missing.
   if (!fake && !omero) {
     Location.checkValidId(id);
     if (new Location(id).length() == 0) {
       LOGGER.error("File has length 0 and may be corrupt");
     }
   }

    if (!id.equals(currentId)) {
      // initialize file
      boolean success = false;
      if (!invalid) {
        for (int i=0; i<readers.length; i++) {
          if (isThisType(readers[i], id, allowOpen)) {
            current = i;
            currentId = id;
            success = true;
            break;
          }
        }
      }
      if (!success) {
        throw new UnknownFormatException("Unknown file format: " + id);
      }
    }
    return getReader();
  }

  /** Gets the reader used to open the current file. */
  public IFormatReader getReader() {
    FormatTools.assertId(currentId, true, 2);
    return readers[current];
  }

  /** Gets the file format reader instance matching the given class. */
  public IFormatReader getReader(Class<? extends IFormatReader> c) {
    for (int i=0; i<readers.length; i++) {
      if (readers[i].getClass().equals(c)) return readers[i];
    }
    return null;
  }

  /** Gets all constituent file format readers. */
  public IFormatReader[] getReaders() {
    IFormatReader[] r = new IFormatReader[readers.length];
    System.arraycopy(readers, 0, r, 0, readers.length);
    return r;
  }

  // -- IMetadataConfigurable API methods --

  /* @see loci.formats.IMetadataConfigurable#getSupportedMetadataLevels() */
  @Override
  public Set<MetadataLevel> getSupportedMetadataLevels() {
    return getReaders()[0].getSupportedMetadataLevels();
  }

  /* @see loci.formats.IMetadataConfigurable#getMetadataOptions() */
  @Override
  public MetadataOptions getMetadataOptions() {
    return getReaders()[0].getMetadataOptions();
  }

  /**
   * @see loci.formats.IMetadataConfigurable#setMetadataOptions(MetadataOptions)
   */
  @Override
  public void setMetadataOptions(MetadataOptions options) {
    for (IFormatReader reader : readers) {
      reader.setMetadataOptions(options);
    }
  }

  // -- IFormatReader API methods --

  /* @see IFormatReader#isThisType(String, boolean) */
  @Override
  public boolean isThisType(String name, boolean open) {
    for (int i=0; i<readers.length; i++) {
      if (isThisType(readers[i], name, open)) return true;
    }
    return false;
  }

  /* @see IFormatReader.isThisType(byte[]) */
  @Override
  public boolean isThisType(byte[] block) {
    for (int i=0; i<readers.length; i++) {
      if (isThisType(readers[i], block)) return true;
    }
    return false;
  }

  /* @see IFormatReader.isThisType(RandomAccessInputStream) */
  @Override
  public boolean isThisType(RandomAccessInputStream stream) throws IOException {
    for (int i=0; i<readers.length; i++) {
      if (isThisType(readers[i], stream)) return true;
    }
    return false;
  }

  /* @see IFormatReader#getImageCount() */
  @Override
  public int getImageCount() {
    return getReader().getImageCount();
  }

  /* @see IFormatReader#isRGB() */
  @Override
  public boolean isRGB() {
    return getReader().isRGB();
  }

  /* @see IFormatReader#getSizeX() */
  @Override
  public int getSizeX() {
    return getReader().getSizeX();
  }

  /* @see IFormatReader#getSizeY() */
  @Override
  public int getSizeY() {
    return getReader().getSizeY();
  }

  /* @see IFormatReader#getSizeC() */
  @Override
  public int getSizeC() {
    return getReader().getSizeC();
  }

  /* @see IFormatReader#getSizeZ() */
  @Override
  public int getSizeZ() {
    return getReader().getSizeZ();
  }

  /* @see IFormatReader#getSizeT() */
  @Override
  public int getSizeT() {
    return getReader().getSizeT();
  }

  /* @see IFormatReader#getPixelType() */
  @Override
  public int getPixelType() {
    return getReader().getPixelType();
  }

  /* @see IFormatReader#getBitsPerPixel() */
  @Override
  public int getBitsPerPixel() {
    return getReader().getBitsPerPixel();
  }

  /* @see IFormatReader#getEffectiveSizeC() */
  @Override
  public int getEffectiveSizeC() {
    return getReader().getEffectiveSizeC();
  }

  /* @see IFormatReader#getRGBChannelCount() */
  @Override
  public int getRGBChannelCount() {
    return getReader().getRGBChannelCount();
  }

  /* @see IFormatReader#isIndexed() */
  @Override
  public boolean isIndexed() {
    return getReader().isIndexed();
  }

  /* @see IFormatReader#isFalseColor() */
  @Override
  public boolean isFalseColor() {
    return getReader().isFalseColor();
  }

  /* @see IFormatReader#get8BitLookupTable() */
  @Override
  public byte[][] get8BitLookupTable() throws FormatException, IOException {
    return getReader().get8BitLookupTable();
  }

  /* @see IFormatReader#get16BitLookupTable() */
  @Override
  public short[][] get16BitLookupTable() throws FormatException, IOException {
    return getReader().get16BitLookupTable();
  }

  /* @see IFormatReader#getModuloZ() */
  @Override
  public Modulo getModuloZ() {
    return getReader().getModuloZ();
  }

  /* @see IFormatReader#getModuloC() */
  @Override
  public Modulo getModuloC() {
    return getReader().getModuloC();
  }

  /* @see IFormatReader#getModuloT() */
  @Override
  public Modulo getModuloT() {
    return getReader().getModuloT();
  }

  /* @see IFormatReader#getThumbSizeX() */
  @Override
  public int getThumbSizeX() {
    return getReader().getThumbSizeX();
  }

  /* @see IFormatReader#getThumbSizeY() */
  @Override
  public int getThumbSizeY() {
    return getReader().getThumbSizeY();
  }

  /* @see IFormatReader#isLittleEndian() */
  @Override
  public boolean isLittleEndian() {
    return getReader().isLittleEndian();
  }

  /* @see IFormatReader#getDimensionOrder() */
  @Override
  public String getDimensionOrder() {
    return getReader().getDimensionOrder();
  }

  /* @see IFormatReader#isOrderCertain() */
  @Override
  public boolean isOrderCertain() {
    return getReader().isOrderCertain();
  }

  /* @see IFormatReader#isThumbnailSeries() */
  @Override
  public boolean isThumbnailSeries() {
    return getReader().isThumbnailSeries();
  }

  /* @see IFormatReader#isInterleaved() */
  @Override
  public boolean isInterleaved() {
    return getReader().isInterleaved();
  }

  /* @see IFormatReader#isInterleaved(int) */
  @Override
  public boolean isInterleaved(int subC) {
    return getReader().isInterleaved(subC);
  }

  /* @see IFormatReader#openBytes(int) */
  @Override
  public byte[] openBytes(int no) throws FormatException, IOException {
    return getReader().openBytes(no);
  }

  /* @see IFormatReader#openBytes(int, int, int, int, int) */
  @Override
  public byte[] openBytes(int no, int x, int y, int w, int h)
    throws FormatException, IOException
  {
    return getReader().openBytes(no, x, y, w, h);
  }

  /* @see IFormatReader#openBytes(int, byte[]) */
  @Override
  public byte[] openBytes(int no, byte[] buf)
    throws FormatException, IOException
  {
    return getReader().openBytes(no, buf);
  }

  /* @see IFormatReader#openBytes(int, byte[], int, int, int, int) */
  @Override
  public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h)
    throws FormatException, IOException
  {
    return getReader().openBytes(no, buf, x, y, w, h);
  }

  /* @see IFormatReader#openPlane(int, int, int, int, int) */
  @Override
  public Object openPlane(int no, int x, int y, int w, int h)
    throws FormatException, IOException
  {
    return getReader().openPlane(no, x, y, w, h);
  }

  /* @see IFormatReader#openThumbBytes(int) */
  @Override
  public byte[] openThumbBytes(int no) throws FormatException, IOException {
    return getReader().openThumbBytes(no);
  }

  /* @see IFormatReader#getSeriesCount() */
  @Override
  public int getSeriesCount() {
    return getReader().getSeriesCount();
  }

  /* @see IFormatReader#setSeries(int) */
  @Override
  public void setSeries(int no) {
    getReader().setSeries(no);
  }

  /* @see IFormatReader#getSeries() */
  @Override
  public int getSeries() {
    return getReader().getSeries();
  }

  /* @see IFormatReader#getUsedFiles() */
  @Override
  public String[] getUsedFiles() {
    return getReader().getUsedFiles();
  }

  /* @see IFormatReader#getUsedFiles(boolean) */
  @Override
  public String[] getUsedFiles(boolean noPixels) {
    return getReader().getUsedFiles(noPixels);
  }

  /* @see IFormatReader#getSeriesUsedFiles() */
  @Override
  public String[] getSeriesUsedFiles() {
    return getReader().getSeriesUsedFiles();
  }

  /* @see IFormatReader#getSeriesUsedFiles(boolean) */
  @Override
  public String[] getSeriesUsedFiles(boolean noPixels) {
    return getReader().getSeriesUsedFiles(noPixels);
  }

  /* @see IFormatReader#getAdvancedUsedFiles(boolean) */
  @Override
  public FileInfo[] getAdvancedUsedFiles(boolean noPixels) {
    return getReader().getAdvancedUsedFiles(noPixels);
  }

  /* @see IFormatReader#getAdvancedSeriesUsedFiles(boolean) */
  @Override
  public FileInfo[] getAdvancedSeriesUsedFiles(boolean noPixels) {
    return getReader().getAdvancedSeriesUsedFiles(noPixels);
  }

  /* @see IFormatReader#getIndex(int, int, int) */
  @Override
  public int getIndex(int z, int c, int t) {
    return getReader().getIndex(z, c, t);
  }

  /* @see IFormatReader#getIndex(int, int, int, int, int, int) */
  @Override
  public int getIndex(int z, int c, int t, int moduloZ, int moduloC, int moduloT) {
    return getReader().getIndex(z, c, t, moduloZ, moduloC, moduloT);
  }

  /* @see IFormatReader#getZCTCoords(int) */
  @Override
  public int[] getZCTCoords(int index) {
    return getReader().getZCTCoords(index);
  }

  /* @see IFormatReader#getZCTModuloCoords(int) */
  @Override
  public int[] getZCTModuloCoords(int index) {
    return getReader().getZCTModuloCoords(index);
  }

  /* @see IFormatReader#getMetadataValue(String) */
  @Override
  public Object getMetadataValue(String field) {
    return getReader().getMetadataValue(field);
  }

  /* @see IFormatReader#getSeriesMetadataValue(String) */
  @Override
  public Object getSeriesMetadataValue(String field) {
    return getReader().getSeriesMetadataValue(field);
  }

  /* @see IFormatReader#getGlobalMetadata() */
  @Override
  public Hashtable<String, Object> getGlobalMetadata() {
    return getReader().getGlobalMetadata();
  }

  /* @see IFormatReader#getSeriesMetadata() */
  @Override
  public Hashtable<String, Object> getSeriesMetadata() {
    return getReader().getSeriesMetadata();
  }

  /* @see IFormatReader#getCoreMetadataList() */
  @Override
  public List<CoreMetadata> getCoreMetadataList() {
    return getReader().getCoreMetadataList();
  }

  /* @see IFormatReader#close(boolean) */
  @Override
  public void close(boolean fileOnly) throws IOException {
    for (int i=0; i<readers.length; i++) readers[i].close(fileOnly);
    if (!fileOnly) currentId = null;
  }

  /* @see IFormatReader#setGroupFiles(boolean) */
  @Override
  public void setGroupFiles(boolean group) {
    FormatTools.assertId(currentId, false, 2);
    for (int i=0; i<readers.length; i++) readers[i].setGroupFiles(group);
  }

  /* @see IFormatReader#isGroupFiles() */
  @Override
  public boolean isGroupFiles() {
    // all readers should have same file grouping setting
    return readers[0].isGroupFiles();
  }

  /* @see IFormatReader#fileGroupOption(String) */
  @Override
  public int fileGroupOption(String id) throws FormatException, IOException {
    return getReader(id).fileGroupOption(id);
  }

  /* @see IFormatReader#setFillColor(Byte) */
  @Override
  public void setFillColor(Byte fill) {
    for (IFormatReader r : readers) {
      r.setFillColor(fill);
    }
  }

  /* @see IFormatReader#getFillColor() */
  @Override
  public Byte getFillColor() {
    return getReader().getFillColor();
  }

  /* @see IFormatReader#isMetadataComplete() */
  @Override
  public boolean isMetadataComplete() {
    return getReader().isMetadataComplete();
  }

  /* @see IFormatReader#setNormalized(boolean) */
  @Override
  public void setNormalized(boolean normalize) {
    FormatTools.assertId(currentId, false, 2);
    for (int i=0; i<readers.length; i++) readers[i].setNormalized(normalize);
  }

  /* @see IFormatReader#isNormalized() */
  @Override
  public boolean isNormalized() {
    // NB: all readers should have the same normalization setting
    return readers[0].isNormalized();
  }

  /* @see IFormatReader#setOriginalMetadataPopulated(boolean) */
  @Override
  public void setOriginalMetadataPopulated(boolean populate) {
    FormatTools.assertId(currentId, false, 1);
    for (int i=0; i<readers.length; i++) {
      readers[i].setOriginalMetadataPopulated(populate);
    }
  }

  /* @see IFormatReader#isOriginalMetadataPopulated() */
  @Override
  public boolean isOriginalMetadataPopulated() {
    return readers[0].isOriginalMetadataPopulated();
  }

  /* @see IFormatReader#getCurrentFile() */
  @Override
  public String getCurrentFile() {
    if (currentId == null) return null;
    return getReader().getCurrentFile();
  }

  /* @see IFormatReader#setMetadataFiltered(boolean) */
  @Override
  public void setMetadataFiltered(boolean filter) {
    FormatTools.assertId(currentId, false, 2);
    for (int i=0; i<readers.length; i++) readers[i].setMetadataFiltered(filter);
  }

  /* @see IFormatReader#isMetadataFiltered() */
  @Override
  public boolean isMetadataFiltered() {
    // NB: all readers should have the same metadata filtering setting
    return readers[0].isMetadataFiltered();
  }

  /* @see IFormatReader#setMetadataStore(MetadataStore) */
  @Override
  public void setMetadataStore(MetadataStore store) {
    FormatTools.assertId(currentId, false, 2);
    for (int i=0; i<readers.length; i++) readers[i].setMetadataStore(store);
  }

  /* @see IFormatReader#getMetadataStore() */
  @Override
  public MetadataStore getMetadataStore() {
    return getReader().getMetadataStore();
  }

  /* @see IFormatReader#getMetadataStoreRoot() */
  @Override
  public Object getMetadataStoreRoot() {
    return getReader().getMetadataStoreRoot();
  }

  /* @see IFormatReader#getUnderlyingReaders() */
  @Override
  public IFormatReader[] getUnderlyingReaders() {
    return getReaders();
  }

  /* @see IFormatReader#isSingleFile(String) */
  @Override
  public boolean isSingleFile(String id) throws FormatException, IOException {
    return getReader(id).isSingleFile(id);
  }

  /* @see IFormatReader#getRequiredDirectories(String[]) */
  @Override
  public int getRequiredDirectories(String[] files)
    throws FormatException, IOException
  {
    return getReader(files[0]).getRequiredDirectories(files);
  }

  /* @see IFormatReader#getDatasetStructureDescription() */
  @Override
  public String getDatasetStructureDescription() {
    return getReader().getDatasetStructureDescription();
  }

  /* @see IFormatReader#hasCompanionFiles() */
  @Override
  public boolean hasCompanionFiles() {
    return getReader().hasCompanionFiles();
  }

  /* @see IFormatReader#getPossibleDomains(String) */
  @Override
  public String[] getPossibleDomains(String id)
    throws FormatException, IOException
  {
    return getReader(id).getPossibleDomains(id);
  }

  /* @see IFormatReader#getDomains() */
  @Override
  public String[] getDomains() {
    return getReader().getDomains();
  }

  /* @see IFormatReader#getOptimalTileWidth() */
  @Override
  public int getOptimalTileWidth() {
    return getReader().getOptimalTileWidth();
  }

  /* @see IFormatReader#getOptimalTileHeight() */
  @Override
  public int getOptimalTileHeight() {
    return getReader().getOptimalTileHeight();
  }

  /* @see IFormatReader#getCoreIndex() */
  @Override
  public int getCoreIndex() {
    return getReader().getCoreIndex();
  }

  /* @see IFormatReader#setCoreIndex(int) */
  @Override
  public void setCoreIndex(int no) {
    getReader().setCoreIndex(no);
  }

  @Override
  public int seriesToCoreIndex(int series) {
    return getReader().seriesToCoreIndex(series);
  }

  @Override
  public int coreIndexToSeries(int index) {
    return getReader().coreIndexToSeries(index);
  }

  /* @see IFormatReader#getResolutionCount() */
  @Override
  public int getResolutionCount() {
    return getReader().getResolutionCount();
  }

  /* @see IFormatReader#setResolution(int) */
  @Override
  public void setResolution(int no) {
    getReader().setResolution(no);
  }

  /* @see IFormatReader#getResolution() */
  @Override
  public int getResolution() {
    return getReader().getResolution();
  }

  /* @see IFormatReader#hasFlattenedResolutions() */
  @Override
  public boolean hasFlattenedResolutions() {
    // all readers should have the same flattened setting
    return readers[0].hasFlattenedResolutions();
  }

  /* @see IFormatReader#setFlattenedResolutions(boolean) */
  @Override
  public void setFlattenedResolutions(boolean flattened) {
    for (IFormatReader reader : readers) {
      reader.setFlattenedResolutions(flattened);
    }
  }

  // -- IFormatHandler API methods --

  /* @see IFormatHandler#isThisType(String) */
  @Override
  public boolean isThisType(String name) {
    // if necessary, open the file for further analysis
    // but check isThisType(name, false) first, for efficiency
    return isThisType(name, false) || isThisType(name, true);
  }

  /* @see IFormatHandler#getFormat() */
  @Override
  public String getFormat() { return getReader().getFormat(); }

  /* @see IFormatHandler#getSuffixes() */
  @Override
  public String[] getSuffixes() {
    if (suffixes == null) {
      HashSet<String> suffixSet = new HashSet<String>();
      for (int i=0; i<readers.length; i++) {
        String[] suf = readers[i].getSuffixes();
        for (int j=0; j<suf.length; j++) suffixSet.add(suf[j]);
      }
      suffixes = new String[suffixSet.size()];
      suffixSet.toArray(suffixes);
      Arrays.sort(suffixes);
    }
    return suffixes;
  }

  /* @see IFormatHandler#getNativeDataType() */
  @Override
  public Class<?> getNativeDataType() {
    return getReader().getNativeDataType();
  }

  /* @see IFormatHandler#setId(String) */
  @Override
  public void setId(String id) throws FormatException, IOException {
    IFormatReader currentReader = getReader(id);
    if (!isOmero(id)) {
      LOGGER.info("{} initializing {}",
      currentReader.getClass().getSimpleName(), id);
    }
    currentReader.setId(id);
  }

  /* @see IFormatReader#reopenFile() */
  @Override
  public void reopenFile() throws IOException {
    getReader().reopenFile();
  }

  /* @see IFormatHandler#close() */
  @Override
  public void close() throws IOException { close(false); }

  // -- ICompressedTileReader API methods --

  @Override
  public int getTileRows(int no) {
    return getReader().getTileRows(no);
  }

  @Override
  public int getTileColumns(int no) {
    return getReader().getTileColumns(no);
  }

  @Override
  public byte[] openCompressedBytes(int no, int x, int y) throws FormatException, IOException {
    return getReader().openCompressedBytes(no, x, y);
  }

  @Override
  public byte[] openCompressedBytes(int no, byte[] buf, int x, int y) throws FormatException, IOException {
    return getReader().openCompressedBytes(no, buf, x, y);
  }

  @Override
  public Codec getTileCodec(int no) throws FormatException, IOException {
    return getReader().getTileCodec(no);
  }

  @Override
  public CodecOptions getTileCodecOptions(int no, int x, int y) throws FormatException, IOException {
    return getReader().getTileCodecOptions(no, x, y);
  }

  private boolean isThisType(IFormatReader reader, String name, boolean allowOpen) {
    try {
      return reader.isThisType(name, allowOpen);
    } catch (Exception e) {
      logIsThisTypeError(reader, e);
      return false;
    }
  }

  private boolean isThisType(IFormatReader reader, byte[] block) {
    try {
      return reader.isThisType(block);
    } catch (Exception e) {
      logIsThisTypeError(reader, e);
      return false;
    }
  }

  private boolean isThisType(IFormatReader reader, RandomAccessInputStream rais) {
    try {
      return reader.isThisType(rais);
    } catch (Exception e) {
      logIsThisTypeError(reader, e);
      return false;
    }
  }

  private static AtomicBoolean raisedException = new AtomicBoolean(false);

  private void logIsThisTypeError(IFormatReader reader, Exception e) {
    LOGGER.debug("{} raised on isThisType", reader, e);
    if (raisedException.compareAndSet(false, true)) {
      LOGGER.error(" *** One or more readers is misbehaving." +
                   " See the debug output for more information. e.g.:\n" +
                   "     {} -> {}('{}') ***",
        reader, e.getClass().getName(), e.getMessage());
    }
  }

}
