/*
 * #%L
 * OME Bio-Formats package for reading and converting biological file formats.
 * %%
 * Copyright (C) 2005 - 2017 Open Microscopy Environment:
 *   - Board of Regents of the University of Wisconsin-Madison
 *   - Glencoe Software, Inc.
 *   - University of Dundee
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 2 of the 
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public 
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-2.0.html>.
 * #L%
 */

package loci.formats.in;

import java.io.IOException;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

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

import ome.units.UNITS;
import ome.units.quantity.Length;
import ome.xml.model.enums.IlluminationType;
import ome.xml.model.primitives.Timestamp;

import loci.common.DataTools;
import loci.common.RandomAccessInputStream;
import loci.common.xml.XMLTools;
import loci.formats.CoreMetadata;
import loci.formats.MetadataTools;
import loci.formats.FormatException;
import loci.formats.FormatTools;
import loci.formats.meta.MetadataStore;
import loci.formats.tiff.IFD;
import loci.formats.tiff.PhotoInterp;
import loci.formats.tiff.TiffParser;

/**
 * LeicaSCNReader is the file format reader for Leica SCN TIFF files.
 */

public class LeicaSCNReader extends BaseTiffReader {

  // -- Constants --

  /** Logger for this class. */
  private static final Logger LOGGER =
      LoggerFactory.getLogger(LeicaSCNReader.class);

  private static final String SCHEMA_2010_03 =
    "http://www.leica-microsystems.com/scn/2010/03/10";
  private static final String SCHEMA_2010_10 =
    "http://www.leica-microsystems.com/scn/2010/10/01";

  private static final String[] MODELS_WITHOUT_CORRECTION = {"versa", "leica scn400"};

  // -- Fields --
  LeicaSCNHandler handler;

  // -- Constructor --

  /** Constructs a new LeicaSCN reader. */
  public LeicaSCNReader() {
    super("Leica SCN", new String[] {"scn"});
    domains = new String[] {FormatTools.HISTOLOGY_DOMAIN};
    suffixNecessary = false;
    suffixSufficient = false;
    canSeparateSeries = false;
    noSubresolutions = true;
  }

  // -- IFormatReader API methods --

  /* (non-Javadoc)
   * @see loci.formats.FormatReader#isThisType(java.lang.String, boolean)
   */
  @Override
  public boolean isThisType(String name, boolean open) {
    if (super.isThisType(name, open) && open) {
      try (RandomAccessInputStream stream = new RandomAccessInputStream(name)) {
        TiffParser tiffParser = new TiffParser(stream);
        if (!tiffParser.isValidHeader()) {
          return false;
        }

        String imageDescription = tiffParser.getComment();
        if (imageDescription != null) {
          try {
            // Test if XML is valid SCN metadata
            LeicaSCNHandler handler = new LeicaSCNHandler();
            XMLTools.parseXML(imageDescription, handler);
            return true;
          }
          catch (Exception se) {
            LOGGER.debug("XML parsing failed", se);
          }
        }
      }
      catch (IOException e) {
        LOGGER.debug("I/O exception during isThisType() evaluation.", e);
      }
    }
    return false;
  }

  private int imageIFD(int no) {
    int s = getCoreIndex();
    Image i = handler.imageMap.get(s);

    int[] dims = getZCTCoords(no);
    int dz = dims[0];
    int dc = dims[1];
    int dr = s - getParent(s);

    return i.pixels.lookupDimension(dz, dc, dr).ifd;
  }

  /**
   * @see loci.formats.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
  {
    FormatTools.checkPlaneParameters(this, no, buf.length, x, y, w, h);
    if (tiffParser == null) {
      initTiffParser();
    }
    int ifd = imageIFD(no);
    tiffParser.getSamples(ifds.get(ifd), buf, x, y, w, h);
    return buf;
  }

  /* @see loci.formats.IFormatReader#openThumbBytes(int) */
  @Override
  public byte[] openThumbBytes(int no) throws FormatException, IOException {
    int originalIndex = getCoreIndex();
    Image i = handler.imageMap.get(getCoreIndex());

    setCoreIndex(getParent(originalIndex) + i.imageThumbnail);
    byte[] thumb = FormatTools.openThumbBytes(this, no);
    setCoreIndex(originalIndex);

    return thumb;
  }

  @Override
  public int getThumbSizeX() {
    int originalIndex = getCoreIndex();
    Image i = handler.imageMap.get(getCoreIndex());

    setCoreIndex(getParent(originalIndex) + i.imageThumbnail);
    int size = super.getThumbSizeX();
    setCoreIndex(originalIndex);

    return size;
  }

  @Override
  public int getThumbSizeY() {
    int originalIndex = getCoreIndex();
    Image i = handler.imageMap.get(getCoreIndex());

    setCoreIndex(getParent(originalIndex) + i.imageThumbnail);
    int size = super.getThumbSizeY();
    setCoreIndex(originalIndex);

    return size;
  }

  /* @see loci.formats.IFormatReader#close(boolean) */
  @Override
  public void close(boolean fileOnly) throws IOException {
    super.close(fileOnly);
    handler = null;
    if (!fileOnly) {
    }
  }

  /* @see loci.formats.IFormatReader#getOptimalTileWidth() */
  @Override
  public int getOptimalTileWidth() {
    FormatTools.assertId(currentId, true, 1);
    try {
      return (int) ifds.get(imageIFD(0)).getTileWidth();
    }
    catch (FormatException e) {
      LOGGER.debug("", e);
    }
    return super.getOptimalTileWidth();
  }

  /* @see loci.formats.IFormatReader#getOptimalTileHeight() */
  @Override
  public int getOptimalTileHeight() {
    FormatTools.assertId(currentId, true, 1);
    try {
      return (int) ifds.get(imageIFD(0)).getTileLength();
    }
    catch (FormatException e) {
      LOGGER.debug("", e);
    }
    return super.getOptimalTileHeight();
  }

  // -- Internal BaseTiffReader API methods --

  @Override
  protected void initTiffParser() {
    super.initTiffParser();
    // older .scn files require color correction
    // newer files from Versa systems do not
    if (handler != null) {
      Image i = handler.imageMap.get(0);
      String model = i.devModel == null ? i.devModel : i.devModel.toLowerCase();
      tiffParser.setYCbCrCorrection(!Arrays.asList(MODELS_WITHOUT_CORRECTION).contains(model));
    }
  }

  protected void initCoreMetadata(int series, int resolution) throws FormatException, IOException {
    int pos = core.flattenedIndex(series, resolution);
    ImageCollection c = handler.collection;
    Image i = handler.imageMap.get(pos);

    if (c == null || i == null) {
      throw new FormatException("Error setting core metadata for series " + series + " resolution " + resolution);
    }

    CoreMetadata ms = core.get(series, resolution);

    // repopulate core metadata

    if (resolution == 0) {
      ms.resolutionCount = i.pixels.sizeR;
      if (ms.resolutionCount == 0) {
        ms.resolutionCount = 1;
      }
    }

    Dimension dimension = i.pixels.lookupDimension(0, 0, resolution);
    if (dimension == null) {
      throw new FormatException(
        "No dimension information for subresolution=" + resolution);
    }

    IFD ifd = ifds.get(dimension.ifd);
    PhotoInterp pi = ifd.getPhotometricInterpretation();
    int samples = ifd.getSamplesPerPixel();

    ms.rgb = samples > 1 || pi == PhotoInterp.RGB;
    ms.sizeX = (int) dimension.sizeX;
    ms.sizeY = (int) dimension.sizeY;
    ms.sizeZ = (int) i.pixels.sizeZ;
    ms.sizeT = 1;
    ms.sizeC = ms.rgb ? samples : i.pixels.sizeC;

    if (ms.sizeX == 0) {
      ms.sizeX = (int) ifd.getImageWidth();
    }
    if (ms.sizeY == 0) {
      ms.sizeY = (int) ifd.getImageLength();
    }
    if (ms.sizeZ == 0) {
      ms.sizeZ = 1;
    }
    if (ms.sizeC == 0) {
      ms.sizeC = 1;
    }

    if ((ifd.getImageWidth() != ms.sizeX) || (ifd.getImageLength() != ms.sizeY))
    {
      throw new FormatException("IFD dimensions do not match XML dimensions for image " +
        pos + ": x=" + ifd.getImageWidth() + ", " + ms.sizeX + ", y=" + ifd.getImageLength() +
        ", " + ms.sizeY);
    }

    ms.orderCertain = true;
    ms.littleEndian = ifd.isLittleEndian();
    ms.indexed = pi == PhotoInterp.RGB_PALETTE &&
      (get8BitLookupTable() != null || get16BitLookupTable() != null);
    ms.imageCount = ms.sizeZ * (ms.rgb ? 1 : ms.sizeC);
    ms.pixelType = ifd.getPixelType();
    ms.metadataComplete = true;
    ms.interleaved = false;
    ms.falseColor = false;
    ms.dimensionOrder = "XYCZT";
    ms.thumbnail = i.imageThumbnail == resolution;
  }

  /* @see loci.formats.BaseTiffReader#initStandardMetadata() */
  @Override
  protected void initStandardMetadata() throws FormatException, IOException {
    super.initStandardMetadata();

    tiffParser.setDoCaching(true); // Otherwise getComment() doesn't return the comment.

    String imageDescription = tiffParser.getComment();
    handler = new LeicaSCNHandler();
    if (imageDescription != null) {
      try {
        LOGGER.trace("Image description XML = {}", imageDescription);
        // parse the XML description
        XMLTools.parseXML(imageDescription, handler);
      }
      catch (Exception se) {
        throw new FormatException("Failed to parse XML", se);
      }
    }
    initTiffParser();
    tiffParser.setDoCaching(true);

    int count = handler.count();

    ifds = tiffParser.getMainIFDs();

    if (ifds.size() < count) {
      count = ifds.size();
    }

    core.clear();
    int currentSeries = 0;
    int currentResolution = 0;
    for (int i=0; i<count; i++) {
      if (currentResolution == 0) {
        core.add();
      }
      CoreMetadata ms = new CoreMetadata();
      core.add(currentSeries, ms);
      tiffParser.fillInIFD(ifds.get(handler.IFDMap.get(i)));
      initCoreMetadata(currentSeries, currentResolution);
      currentResolution++;
      if (currentResolution == core.get(currentSeries, 0).resolutionCount) {
        currentSeries++;
        currentResolution = 0;
      }
    }
  }

  /* @see loci.formats.BaseTiffReader#initMetadataStore() */
  @Override
  protected void initMetadataStore() throws FormatException {
    super.initMetadataStore();

    MetadataStore store = makeFilterMetadata();
    MetadataTools.populatePixels(store, this, true);

    HashMap<String,Integer> instrumentIDs = new HashMap<String,Integer>();
    int instrumentidno = 0;
    HashMap<String,String> objectives = new HashMap<String,String>();
    int objectiveidno = 0;

    int pos = 0;
    for (int s=0; s<core.size(); s++) {
      for (int r = 0; r < core.size(s); r++) {
        int coreIndex = core.flattenedIndex(s, r);
        setCoreIndex(coreIndex);
        ImageCollection c = handler.collection;
        Image i = handler.imageMap.get(coreIndex);

        int subresolution = r;
        if (!hasFlattenedResolutions()) {
          subresolution = 0;
        }
        if (!hasFlattenedResolutions() && r > 0) {
          continue;
        }

        Dimension dimension = i.pixels.lookupDimension(0, 0, r);
        if (dimension == null) {
          throw new FormatException(
            "No dimension information for subresolution=" + subresolution);
        }

        // Leica units are nanometres; convert to µm
        double sizeX = i.vSizeX / 1000.0;
        double sizeY = i.vSizeY / 1000.0;
        final Length offsetX = new Length(i.vOffsetX, UNITS.NANOMETER);
        final Length offsetY = new Length(i.vOffsetY, UNITS.NANOMETER);
        double sizeZ = i.vSpacingZ / 1000.0;

        store.setPixelsPhysicalSizeX(
          FormatTools.getPhysicalSizeX(sizeX / dimension.sizeX), pos);
        store.setPixelsPhysicalSizeY(
          FormatTools.getPhysicalSizeY(sizeY / dimension.sizeY), pos);
        store.setPixelsPhysicalSizeZ(FormatTools.getPhysicalSizeZ(sizeZ), pos);

        if (instrumentIDs.get(i.devModel) == null) {
          String instrumentID = MetadataTools.createLSID("Instrument", instrumentidno);
          instrumentIDs.put(i.devModel, instrumentidno);
          store.setInstrumentID(instrumentID, instrumentidno);
          instrumentidno++;
        }

        int inst = instrumentIDs.get(i.devModel);
        String objectiveName = i.devModel + ":" + i.objMag;
        if (objectives.get(objectiveName) == null && i.objMag != null) {
          String objectiveID = MetadataTools.createLSID("Objective", inst, objectiveidno);
          objectives.put(objectiveName, objectiveID);
          store.setObjectiveID(objectiveID, inst, objectiveidno);

          Double mag = Double.parseDouble(i.objMag);
          store.setObjectiveNominalMagnification(mag, inst, objectiveidno);
          store.setObjectiveCalibratedMagnification(mag, inst, objectiveidno);
          store.setObjectiveLensNA(DataTools.parseDouble(i.illumNA), inst, objectiveidno);
          objectiveidno++;
        }

        store.setImageInstrumentRef(
          MetadataTools.createLSID("Instrument", inst), pos);
        if (objectives.containsKey(objectiveName)) {
          store.setObjectiveSettingsID(objectives.get(objectiveName), pos);
        }
        // TODO: Only "brightfield" has been seen in example files
        if (i.illumSource != null && i.illumSource.equals("brightfield")) {
          store.setChannelIlluminationType(IlluminationType.TRANSMITTED, pos, 0);
        } else {
          store.setChannelIlluminationType(IlluminationType.OTHER, pos, 0);
          LOGGER.debug("Unknown illumination source {}", i.illumSource);
        }

        CoreMetadata ms = core.get(s, r);

        for (int q = 0; q < ms.imageCount; q++) {
          store.setPlanePositionX(offsetX, pos, q);
          store.setPlanePositionY(offsetY, pos, q);
        }

        if (hasFlattenedResolutions()) {
          store.setImageName(i.name + " (R" + subresolution + ")", pos);
        }
        else {
          if (pos == 0) {
            // assume first image (usually a pyramid) is the macro image
            // the value of i.name will not reliably identify a macro
            store.setImageName("macro", pos);
          }
          else if (ms.resolutionCount > 1) {
            store.setImageName("", pos);
          }
          else {
            store.setImageName(i.name, pos);
          }
        }
        store.setImageDescription("Collection " + c.name, pos);

        if (i.creationDate != null) {
          store.setImageAcquisitionDate(new Timestamp(i.creationDate), pos);
        }

        // Original metadata...
        addSeriesMeta("collection.name", c.name);
        addSeriesMeta("collection.uuid", c.uuid);
        addSeriesMeta("collection.barcode", c.barcode);
        addSeriesMeta("collection.ocr", c.ocr);
        addSeriesMeta("creationDate", i.creationDate);

        addSeriesMeta("device.model for image", i.devModel);
        addSeriesMeta("device.version for image", i.devVersion);
        addSeriesMeta("view.sizeX for image", i.vSizeX);
        addSeriesMeta("view.sizeY for image", i.vSizeY);
        addSeriesMeta("view.offsetX for image", i.vOffsetX);
        addSeriesMeta("view.offsetY for image", i.vOffsetY);
        addSeriesMeta("view.spacingZ for image", i.vSpacingZ);
        addSeriesMeta("scanSettings.objectiveSettings.objective for image", i.objMag);
        addSeriesMeta("scanSettings.illuminationSettings.numericalAperture for image", i.illumNA);
        addSeriesMeta("scanSettings.illuminationSettings.illuminationSource for image", i.illumSource);

        ++pos;
      }
    }
    setCoreIndex(0);
  }

  private int getParent(int coreIndex) {
    int[] pos = core.flattenedIndexes(coreIndex);
    return core.flattenedIndex(pos[0], 0);
  }

  /**
   * SAX handler for parsing XML in Leica SCN files.
   *
   * @author Roger Leigh <r.leigh at dundee.ac.uk>
   */
  class LeicaSCNHandler extends DefaultHandler {

    // -- Fields --
    boolean valid = false;

    public ImageCollection collection;
    public Image currentImage;
    public int seriesIndex;
    public ArrayList<Integer> IFDMap = new ArrayList<Integer>();
    public ArrayList<Image> imageMap = new ArrayList<Image>();

    // Stack of XML elements to keep track of placement in the tree.
    public Deque<String> nameStack = new ArrayDeque<String>();
    // CDATA text stored while parsing.  Note that this is limited to a
    // single span between two tags, and CDATA with embedded elements is
    // not supported.
    public String cdata;

    public int resolutionCount = 0;

    // -- DefaultHandler API methods --

    @Override
    public void endElement(String uri, String localName, String qName) {
      if (!nameStack.isEmpty() && nameStack.peek().equals(qName)) {
        nameStack.pop();
      }

      if (qName.equals("image")) {
        currentImage.imageNumStart = seriesIndex;
        seriesIndex += currentImage.pixels.sizeR *
          currentImage.pixels.sizeC * currentImage.pixels.sizeZ;
        currentImage.imageNumEnd = seriesIndex - 1;
        resolutionCount += currentImage.pixels.sizeR;
        currentImage = null;
      }
      else if (qName.equals("creationDate")) {
        currentImage.creationDate = cdata;
      }
      else if (qName.equals("pixels")) {
        // Compute size of C, R and Z
        Pixels p = currentImage.pixels;
        int sizeC = 0;
        int sizeR = 0;
        int sizeZ = 0;

        for (Dimension d : p.dims) {
          if (d.c > sizeC) {
            sizeC = d.c;
          }
          if (d.r > sizeR) {
            sizeR = d.r;
          }
          if (d.z > sizeZ) {
            sizeZ = d.z;
          }
        }
        sizeC++;
        sizeR++;
        sizeZ++;

        // Set up storage for all dimensions.
        p.sizeC = sizeC;
        p.sizeR = sizeR;
        p.sizeZ = sizeZ;

        for (Dimension d : p.dims) {
          if (d.r == 0 || currentImage.thumbSizeX > d.sizeX) {
            currentImage.thumbSizeX = d.sizeX;
            currentImage.imageThumbnail = d.r;
          }
        }

        // Dimension ordering indirection (R=image, then Z, then C)
        for (int cr = 0; cr < sizeR; cr++) {
          imageMap.add(currentImage);
          for (int cc = 0; cc < sizeC; cc++) {
            for (int cz = 0; cz < sizeZ; cz++) {
              IFDMap.add(p.lookupDimension(cz, cc, cr).ifd);
            }
          }
        }
      }
      else if (qName.equals("objective")) {
        currentImage.objMag = cdata;
      }
      else if (qName.equals("numericalAperture")) {
        currentImage.illumNA = cdata;
      }
      else if (qName.equals("illuminationSource")) {
        currentImage.illumSource = cdata;
      }
      cdata = null;
    }

    @Override
    public void characters(char[] ch, int start, int length) {
      String s = new String(ch, start, length);
      if (cdata == null) {
        cdata = s;
      }
      else {
        cdata += s;
      }
    }

    @Override
    public void startElement(String uri, String localName, String qName,
      Attributes attributes) throws SAXException
    {
      cdata = null;

      if (qName.equals("scn")) {
        String ns = attributes.getValue("xmlns");
        if (ns == null) {
          throw new SAXException("Invalid Leica SCN XML");
        }
        if (!(ns.equals(SCHEMA_2010_03) || ns.equals(SCHEMA_2010_10))) {
          LOGGER.warn("Unknown Leica SCN XML schema: " + ns + "; this file may not be read correctly");
        }

        valid = true;
        seriesIndex = 0;
      }

      if (!valid) {
        throw new SAXException("Invalid Leica SCN XML");
      }

      if (qName.equals("collection")) {
        collection = new ImageCollection(attributes);
      }
      else if (qName.equals("image")) {
        currentImage = new Image(attributes);
        collection.images.add(currentImage);
      }
      else if (qName.equals("device")) {
        currentImage.devModel = attributes.getValue("model");
        currentImage.devVersion = attributes.getValue("version");
      }
      else if (qName.equals("pixels")) {
        if (currentImage.pixels == null) {
          currentImage.pixels = new Pixels(attributes);
        }
        else {
          throw new SAXException("Invalid Leica SCN XML: Multiple pixels elements for single image");
        }
      }
      else if (qName.equals("dimension")) {
        currentImage.pixels.dims.add(new Dimension(attributes));
      }
      else if (qName.equals("view")) {
        currentImage.setView(attributes);
      }
      else if (qName.equals("supplementalImage")) {
        // supplemental images (barcodes etc.) only have
        // a type (name) and IFD index

        String type = attributes.getValue("type");
        String ifd = attributes.getValue("ifd");

        if (ifd != null) {
          currentImage = new Image(attributes);
          currentImage.pixels = new Pixels(attributes);
          Dimension dim = new Dimension(attributes);
          currentImage.pixels.dims.add(dim);
          currentImage.name = type;
          collection.images.add(currentImage);
          imageMap.add(currentImage);
          IFDMap.add(Integer.parseInt(ifd));
          resolutionCount++;
        }
      }

      nameStack.push(qName);
    }

    int count() {
      return resolutionCount;
    }
  }

  class ImageCollection {
    String name;
    String uuid;
    long sizeX;
    long sizeY;
    String barcode;
    String ocr;

    ArrayList<Image> images;

    ImageCollection(Attributes attrs) {
      name = attrs.getValue("name");
      uuid = attrs.getValue("uuid");
      String s = attrs.getValue("sizeX");
      if (s != null) {
        sizeX = Long.parseLong(s);
      }
      s = attrs.getValue("sizeY");
      if (s != null) {
        sizeY = Long.parseLong(s);
      }
      barcode = attrs.getValue("barcode");
      ocr = attrs.getValue("ocr");

      images = new ArrayList<Image>();
    }
  }

  class Image {
    int imageNumStart; // first image number
    int imageNumEnd;   // last image number (subresolutions)
    int imageThumbnail; // image for thumbnailing
    long thumbSizeX;

    String name;
    String uuid;
    String creationDate;
    String devModel; // device model
    String devVersion; // device version
    Pixels pixels; // pixel metadata for each subresolution
    long vSizeX; // view size x (nm)
    long vSizeY; // view size y (nm)
    long vOffsetX; // view offset x (nm?)
    long vOffsetY; // view offset y (nm?)
    long vSpacingZ; // view spacing z (nm?)
    String objMag; // objective magnification
    String illumNA; // illumination NA (why not objective NA?)
    String illumSource; // illumination source

    Image(Attributes attrs) {
      name = attrs.getValue("name");
      uuid = attrs.getValue("uuid");
    }

    void setView(Attributes attrs) {
      String s = attrs.getValue("sizeX");
      if (s != null) {
        vSizeX = Long.parseLong(s);
      }
      s = attrs.getValue("sizeY");
      if (s != null) {
        vSizeY = Long.parseLong(s);
      }
      s = attrs.getValue("offsetX");
      if (s != null) {
        vOffsetX = Long.parseLong(s);
      }
      s = attrs.getValue("offsetY");
      if (s != null) {
        vOffsetY = Long.parseLong(s);
      }
      s = attrs.getValue("spacingZ");
      if (s != null) {
        vSpacingZ = Long.parseLong(s);
      }
    }
  }

  class Pixels {
    // Set up storage for each resolution and each dimension.  Set main resolution.

    // data order (XYCRZ) [unused; force to XYCZT]
    // sizeX, sizeY
    // sizeZ, sizeC, sizeR [unused; compute from dimensions]
    // firstIFD (number) [unused]
    // dimension->IFD mapping (RZC to sizeX, sizeY, IFD)
    //   use 3 arrays of size C*Z*R

    ArrayList<Dimension> dims = new ArrayList<Dimension>();
    long sizeX;
    long sizeY;
    int sizeZ;
    int sizeC;
    int sizeR;
    int lastIFD;

    Pixels(Attributes attrs) {
      // Set main resolution.
      String s = attrs.getValue("sizeX");
      if (s != null) {
        sizeX = Long.parseLong(s);
      }
      s = attrs.getValue("sizeY");
      if (s != null) {
        sizeY = Long.parseLong(s);
      }
    }

    public Dimension lookupDimension(int z, int c, int resolution) {
      for (Dimension d : dims) {
        if (d.z == z && d.c == c && d.r == resolution) {
          return d;
        }
      }
      return null;
    }
  }

  class Dimension {
    // Single image plane for given Z, C, R dimensions
    long sizeX = 0;
    long sizeY = 0;
    int z = 0;
    int c = 0;
    int r = 0;
    int ifd = 0;

    Dimension(Attributes attrs) {
      String s = attrs.getValue("r");
      if (s != null) {
        r = Integer.parseInt(s);
      }
      s = attrs.getValue("z");
      if (s != null) {
        z = Integer.parseInt(s);
      }
      s = attrs.getValue("c");
      if (s != null) {
        c = Integer.parseInt(s);
      }
      s = attrs.getValue("sizeX");
      if (s != null) {
        sizeX = Long.parseLong(s);
      }
      s = attrs.getValue("sizeY");
      if (s != null) {
        sizeY = Long.parseLong(s);
      }
      s = attrs.getValue("ifd");
      if (s != null) {
        ifd = Integer.parseInt(s);
      }
    }
  }
}
