/*
 * #%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.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;

import loci.common.Location;
import loci.common.RandomAccessInputStream;
import loci.common.services.DependencyException;
import loci.common.services.ServiceException;
import loci.common.services.ServiceFactory;
import loci.common.xml.XMLTools;
import loci.formats.CoreMetadata;
import loci.formats.FormatException;
import loci.formats.FormatReader;
import loci.formats.FormatTools;
import loci.formats.MetadataTools;
import loci.formats.meta.MetadataStore;
import loci.formats.ome.OMEXMLMetadata;
import loci.formats.services.OMEXMLService;

import ome.xml.meta.MetadataConverter;
import ome.xml.meta.OMEXMLMetadataRoot;
import ome.xml.model.Image;
import ome.xml.model.primitives.Color;
import ome.xml.model.primitives.NonNegativeInteger;
import ome.xml.model.primitives.PositiveInteger;
import ome.xml.model.primitives.Timestamp;

import ome.units.quantity.Length;
import ome.units.quantity.Time;
import ome.units.UNITS;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * A bioformat reader for the Yokagawa Cellvoyager CV1000 automated microscope (
 * {@linkplain <a href="http://www.yokogawa.com/scanner/products/cv1000e.htm">http://www.yokogawa.com/scanner/products/cv1000e.htm</a>}).
 * <p>
 * This reader opens the dataset generated by the Yokagawa Cellvoyager automated
 * microscope. The user should point the reader to the file named
 * <code>MeasurementResult.xml</code> in the dataset folder.
 * <p>
 * The file format itself consists of several nested folder containing a lot of
 * - sometimes redundant - information. This specific reader exploits the
 * following files:
 * <ul>
 * <li><code>MeasurementResult.xml</code> that contains high level information
 * about image geometry, well (if any) organization, fields arrangement, etc..
 * <li><code>MeasurementResult.ome.xml</code> is a malformed OME xml file that
 * contains information on one of the numerous TIF files that compose the pixel
 * data. It is used to easily extract common metadata.
 * <li>All the TIF files in the <code>Image</code> folder: They contain the
 * pixel data itself. Each well (if any) can have several Areas (fields in the
 * HCS vocable), in turn composed of several tiles that are to be stitched
 * border to border, without the possibility of a more subtle stitching process.
 * </ul>
 * This reader stitches each area on the fly upon loading. It yields one series
 * per well x area. If there is 20 wells each made of 2 areas, 40 series will be
 * available.
 * <p>
 * This reader version was derived from reverse-engineering data files generated
 * on a system hosted by the imaging facility of the Institut Pasteur, Paris
 * (Imagopole / PFID). It spits file versioned <code>1.0</code>. The file format
 * contains several obvious typos, notably in some XML tags, and as mentionned
 * above, the <code>ome.xml</code> is malformed. It is likely that that
 * subsequent versions of the CellVoyager format will fix these problems, alas
 * incapacitating this reader. Should it happen, mail the author to help them
 * update this reader.
 *
 * @author Jean-Yves Tinevez <jeanyves.tinevez@gmail.com> Oct-Nov 2013
 * @author Melissa Linkert
 */
public class CellVoyagerReader extends FormatReader
{

  private static final String SINGLE_TIFF_PATH_BUILDER = "W%dF%03dT%04dZ%02dC%d.tif";

  private String imageFolder;

  private List< ChannelInfo > channelInfos;

  private List< WellInfo > wells;

  private List< Integer > timePoints;

  private String measurementResultFile;

  private String omeMeasurementFile;

  public CellVoyagerReader()
  {
    super( "CellVoyager", new String[] { "tif", "xml" } );
    this.hasCompanionFiles = true;
    this.datasetDescription = "Directory with 2 master files 'MeasurementResult.xml' and 'MeasurementResult.ome.xml', used to stitch together several TIF files.";
    this.domains = new String[] { FormatTools.HISTOLOGY_DOMAIN, FormatTools.LM_DOMAIN, FormatTools.HCS_DOMAIN };
  }

  @Override
  public byte[] openBytes( final int no, final byte[] buf, final int x, final int y, final int w, final int h ) throws FormatException, IOException
  {
    FormatTools.checkPlaneParameters( this, no, buf.length, x, y, w, h );

    final CoreMetadata cm = core.get( getSeries() );

    int[] zct = getZCTCoords(no);

    final int[] indices = seriesToWellArea( getSeries() );
    final int wellIndex = indices[ 0 ];
    final int areaIndex = indices[ 1 ];
    final WellInfo well = wells.get( wellIndex );
    final AreaInfo area = well.areas.get( areaIndex );
    final MinimalTiffReader tiffReader = new MinimalTiffReader();

    for ( final FieldInfo field : area.fields )
    {

      String filename = String.format(SINGLE_TIFF_PATH_BUILDER, wellIndex + 1, field.index, zct[2] + 1, zct[0] + 1, zct[1] + 1);
      final Location image = new Location( imageFolder, filename );
      if (!image.exists()) {
        LOGGER.warn("Could not find file {}", image);
        continue;
      }

      try {
        tiffReader.setId( image.getAbsolutePath() );

        // Tile size
        final int tw = channelInfos.get( 0 ).tileWidth;
        final int th = channelInfos.get( 0 ).tileHeight;

        // Field bounds in full final image, full width, full height
        // (referential named '0', as if x=0 and y=0).
        final int xbs0 = ( int ) field.xpixels;
        final int ybs0 = ( int ) field.ypixels;

        // Subimage bounds in full final image is simply x, y, x+w, y+h

        // Do they intersect?
        if ( x + w < xbs0 || xbs0 + tw < x || y + h < ybs0 || ybs0 + th < y )
        {
          continue;
        }

        // Common rectangle in reconstructed image referential.
        final int xs0 = Math.max( xbs0 - x, 0 );
        final int ys0 = Math.max( ybs0 - y, 0 );

        // Common rectangle in tile referential (named with '1').
        final int xs1 = Math.max( x - xbs0, 0 );
        final int ys1 = Math.max( y - ybs0, 0 );
        final int xe1 = Math.min( tw, x + w - xbs0 );
        final int ye1 = Math.min( th, y + h - ybs0 );
        final int w1 = xe1 - xs1;
        final int h1 = ye1 - ys1;

        if ( w1 <= 0 || h1 <= 0 )
        {
          continue;
        }

        // Get corresponding data.
        final byte[] bytes = tiffReader.openBytes( 0, xs1, ys1, w1, h1 );
        final int nbpp = cm.bitsPerPixel / 8;

        for ( int row1 = 0; row1 < h1; row1++ )
        {
          // Line index in tile coords
          final int ls1 = nbpp * ( row1 * w1 );
          final int length = nbpp * w1;

          // Line index in reconstructed image coords
          final int ls0 = nbpp * ( ( ys0 + row1 ) * w + xs0 );

          // Transfer
          System.arraycopy( bytes, ls1, buf, ls0, length );
        }
      }
      finally {
        tiffReader.close();
      }
    }

    return buf;
  }

  @Override
  public int fileGroupOption( final String id ) throws FormatException, IOException
  {
    return FormatTools.MUST_GROUP;
  }

  @Override
  public int getRequiredDirectories( final String[] files ) throws FormatException, IOException
  {
    /*
     * We only need the directory where there is the two xml files. The
     * parent durectory seems to contain only hardware macros to load and
     * eject the plate or slide.
     */
    return 0;
  }

  @Override
  public boolean isSingleFile( final String id ) throws FormatException, IOException
  {
    return false;
  }

  @Override
  public boolean isThisType( final String name, final boolean open )
  {
    /*
     * We want to be pointed to any file in the directory that contains
     * 'MeasurementResult.xml'.
     */
    final String localName = new Location( name ).getName();
    if ( localName.equals( "MeasurementResult.xml" ) ) { return true; }
    final Location parent = new Location( name ).getAbsoluteFile().getParentFile();
    Location xml = new Location( parent, "MeasurementResult.xml" );
    if (!xml.exists() && parent != null) {
      if (parent.getParent() == null) {
        return false;
      }
      xml = new Location(parent.getParentFile(), "MeasurementResult.xml");
      if (!xml.exists()) {
        return false;
      }
    }

    return super.isThisType( name, open );
  }

  @Override
  protected void initFile( final String id ) throws FormatException, IOException
  {
    super.initFile( id );

    Location measurementFolder = new Location( id ).getAbsoluteFile();
    if ( !measurementFolder.exists() ) { throw new IOException( "File " + id + " does not exist." ); }
    if ( !measurementFolder.isDirectory() )
    {
      measurementFolder = measurementFolder.getParentFile();
      if (measurementFolder.getName().equals("Image")) {
        measurementFolder = measurementFolder.getParentFile();
      }
    }
    imageFolder = new Location(measurementFolder, "Image").getAbsolutePath();

    Location measurementResult = new Location( measurementFolder, "MeasurementResult.xml" );
    if ( !measurementResult.exists() ) { throw new IOException( "Could not find " + measurementResult + " in folder." ); }
    else {
      measurementResultFile = measurementResult.getAbsolutePath();
    }

    Location omeMeasurement = new Location( measurementFolder, "MeasurementResult.ome.xml" );
    if ( !omeMeasurement.exists() ) { throw new IOException( "Could not find " + omeMeasurement + " in folder." ); }
    else {
      omeMeasurementFile = omeMeasurement.getAbsolutePath();
    }

    /*
     * Open MeasurementSettings file
     */

    Document msDocument = null;
    try (RandomAccessInputStream result = new RandomAccessInputStream(measurementResultFile)) {
      msDocument = XMLTools.parseDOM(result);
    }
    catch (ParserConfigurationException e) {
      throw new IOException(e);
    }
    catch (SAXException e) {
      throw new IOException(e);
    }

    msDocument.getDocumentElement().normalize();

    /*
     * Check file version
     */

    final String fileVersionMajor = getChildText( msDocument.getDocumentElement(), new String[] { "FileVersion", "Major" } );
    // Note the typo here.
    final String fileVersionMinor = getChildText( msDocument.getDocumentElement(), new String[] { "FileVersion", "Miner" } );
    if ( !fileVersionMajor.equals( "1" ) || !fileVersionMinor.equals( "0" ) )
    {
      LOGGER.warn( "Detected a file version " + fileVersionMajor + "." + fileVersionMinor + ". This reader was built by reverse-engineering v1.0 files only. Errors might occur." );
    }

    /*
     * Open OME metadata file
     */


    Document omeDocument = null;
    try (RandomAccessInputStream measurement = new RandomAccessInputStream(omeMeasurementFile)) {
      omeDocument = XMLTools.parseDOM(measurement);
    }
    catch (ParserConfigurationException e) {
      throw new IOException(e);
    }
    catch (SAXException e) {
      throw new IOException(e);
    }
    omeDocument.getDocumentElement().normalize();

    /*
     * Extract metadata from MeasurementSetting.xml & OME xml file. This is
     * where the core of parsing and fetching useful info happens
     */

    readInfo( msDocument, omeDocument );
  }

  @Override
  public String[] getSeriesUsedFiles( final boolean noPixels )
  {
    FormatTools.assertId( currentId, true, 1 );

    if ( noPixels )
    {
      return new String[] { measurementResultFile, omeMeasurementFile };
    }
    else
    {
      final int[] indices = seriesToWellArea( getSeries() );
      final int wellIndex = indices[ 0 ];
      final int areaIndex = indices[ 1 ];
      final AreaInfo area = wells.get( wellIndex ).areas.get( areaIndex );
      final int nFields = area.fields.size();
      ArrayList<String> images = new ArrayList<String>();
      images.add(measurementResultFile);
      images.add(omeMeasurementFile);

      for ( final Integer timepoint : timePoints )
      {
        for ( int zslice = 1; zslice <= getSizeZ(); zslice++ )
        {
          for ( int channel = 1; channel <= getSizeC(); channel++ )
          {
            for ( final FieldInfo field : area.fields )
            {
              /*
               * Here we compose file names on the fly assuming
               * they follow the pattern below. Fragile I guess.
               */
              String relativePath = String.format(SINGLE_TIFF_PATH_BUILDER,
                wellIndex + 1, field.index, timepoint, zslice, channel);
              Location imageFile = new Location(imageFolder, relativePath);
              if (imageFile.exists()) {
                images.add(imageFile.getAbsolutePath());
              }
            }
          }
        }
      }
      return images.toArray(new String[images.size()]);
    }

  }

  /*
   * PRIVATE METHODS
   */

  /**
   * Returns the well index (in the field {@link #wells}) and the area index
   * (in the field {@link WellInfo#areas} corresponding to the specified
   * series.
   *
   * @param series
   *            the desired series.
   * @return the corresponding well index and area index, as a 2-element array
   *         of <code>int[] { well, area }</code>.
   */
  private int[] seriesToWellArea( final int series )
  {
    int nWell = -1;
    int seriesInc = -1;
    for ( final WellInfo well : wells )
    {
      nWell++;
      int nAreas = -1;
      for ( @SuppressWarnings( "unused" )
      final AreaInfo area : well.areas )
      {
        seriesInc++;
        nAreas++;
        if ( series == seriesInc ) { return new int[] { nWell, nAreas }; }
      }
    }
    throw new IllegalStateException( "Cannot find a well for series " + series );
  }

  private void readInfo( final Document msDocument, final Document omeDocument ) throws FormatException
  {
    /*
     * Magnification.
     *
     * We need it early, because the file format reports only un-magnified
     * sizes. So if we are to put proper metadata, we need to make the
     * conversion to size measured at the sample level ourselves. I feel
     * like this is fragile and most likely to change in a future version of
     * the file format.
     */

    final Element msRoot = msDocument.getDocumentElement();
    final double objectiveMagnification = Double.parseDouble( getChildText( msRoot, new String[] { "ObjectiveLens", "Magnification" } ) );
    // final double zoomLensMagnification = Double.parseDouble(
    // getChildText( msRoot, new String[] { "ZoomLens", "Magnification",
    // "Value" } ) );
    final double magnification = objectiveMagnification; // *
    // zoomLensMagnification;

    /*
     * Read the ome.xml file. Since it is malformed, we need to parse all
     * nodes, and add an "ID" attribute to those who do not have it.
     */

    final NodeList nodeList = omeDocument.getElementsByTagName( "*" );
    for ( int i = 0; i < nodeList.getLength(); i++ )
    {
      final Node node = nodeList.item( i );
      if ( node.getNodeType() == Node.ELEMENT_NODE )
      {
        final NamedNodeMap atts = node.getAttributes();

        final Node namedItem = atts.getNamedItem( "ID" );
        if ( namedItem == null )
        {
          final String name = node.getNodeName();
          final String id = name + ":" + i;
          if ( !node.getParentNode().getNodeName().equals( "LightSource" ) )
          {
            ( ( Element ) node ).setAttribute( "ID", id );
          }
        }
      }
    }

    /*
     * For single-slice image, the PhysicalSizeZ can be 0, which will make
     * the metadata read fail. Correct that.
     */

    final Element pszEl = getChild( omeDocument.getDocumentElement(), new String[] { "Image", "Pixels" } );
    final double physicalSizeZ = Double.parseDouble( pszEl.getAttribute( "PhysicalSizeZ" ) );
    if ( physicalSizeZ <= 0 )
    {
      // default to 1 whatever
      pszEl.setAttribute( "PhysicalSizeZ", "" + 1 );
    }

    /*
     * Now that the XML document is properly formed, we can build a metadata
     * object from it.
     */

    OMEXMLService service = null;
    String xml = null;
    try
    {
      xml = XMLTools.getXML( omeDocument );
    }
    catch ( final TransformerConfigurationException e2 )
    {
      LOGGER.debug( "", e2 );
    }
    catch ( final TransformerException e2 )
    {
      LOGGER.debug("", e2);
    }
    try
    {
      service = new ServiceFactory().getInstance( OMEXMLService.class );
    }
    catch ( final DependencyException e1 )
    {
      LOGGER.debug( "", e1 );
    }
    OMEXMLMetadata omeMD = null;
    try
    {
      omeMD = service.createOMEXMLMetadata( xml );
    }
    catch ( final ServiceException e )
    {
      LOGGER.debug( "", e );
    }
    catch ( final NullPointerException npe )
    {
      LOGGER.debug( "", npe );
      throw npe;
    }

    // Correct pixel size for magnification
    omeMD.setPixelsPhysicalSizeX( FormatTools.createLength( omeMD.getPixelsPhysicalSizeX( 0 ).value().doubleValue() / magnification , omeMD.getPixelsPhysicalSizeX( 0 ).unit()), 0 );
    omeMD.setPixelsPhysicalSizeY( FormatTools.createLength( omeMD.getPixelsPhysicalSizeY( 0 ).value().doubleValue() / magnification , omeMD.getPixelsPhysicalSizeY( 0 ).unit()), 0 );

    // Time interval
    if (Double.valueOf( readFrameInterval( msDocument ) ) != null) {
      omeMD.setPixelsTimeIncrement(new Time( Double.valueOf( readFrameInterval( msDocument ) ), UNITS.SECOND), 0 );
    }

    /*
     * Channels
     */

    final Element channelsEl = getChild( msRoot, "Channels" );
    final List< Element > channelEls = getChildren( channelsEl, "Channel" );
    channelInfos = new ArrayList< ChannelInfo >();
    for ( final Element channelEl : channelEls )
    {
      final boolean isEnabled = Boolean.parseBoolean( getChildText( channelEl, "IsEnabled" ) );
      if ( !isEnabled )
      {
        continue;
      }
      final ChannelInfo ci = readChannel( channelEl );
      channelInfos.add( ci );
    }

    /*
     * Fix missing IDs.
     *
     * Some IDs are missing in the malformed OME.XML file. We must put them
     * back manually. Some are fixed here
     */


    final Element containerEl = getChild(msRoot, new String[] {
      "Attachment", "HolderInfoList", "HolderInfo", "MountedSampleContainer" });
    final String type = containerEl.getAttribute("xsi:type");
    boolean plateMetadata = type.equals("WellPlate");

    // fix the Plate and Screen IDs if this is a plate dataset
    // otherwise remove the extra Plate and Screen to avoid confusion
    if (plateMetadata) {
      omeMD.setScreenID(MetadataTools.createLSID("Screen", 0), 0);
      omeMD.setPlateID(MetadataTools.createLSID("Plate", 0), 0);
    }
    else {
      OMEXMLMetadataRoot root = (OMEXMLMetadataRoot) omeMD.getRoot();
      root.removeScreen(root.getScreen(0));
      root.removePlate(root.getPlate(0));
      omeMD.setRoot(root);
    }

    omeMD.setProjectID( MetadataTools.createLSID( "Project", 0 ), 0 );
    omeMD.setInstrumentID( MetadataTools.createLSID( "Instrument", 0 ), 0 );

    // Read pixel sizes from OME metadata.
    final double pixelWidth = omeMD.getPixelsPhysicalSizeX( 0 ).value().doubleValue();
    final double pixelHeight = omeMD.getPixelsPhysicalSizeY( 0 ).value().doubleValue();

    /*
     * Read tile size from channel info. This is weird, but it's like that.
     * Since we build a multi-C image, we have to assume that all channels
     * have the same dimension, even if the file format allows for changing
     * the size, binning, etc. from channel to channel. Failure to load
     * datasets that have this exoticity is to be sought here.
     */

    final int tileWidth = channelInfos.get( 0 ).tileWidth;
    final int tileHeight = channelInfos.get( 0 ).tileHeight;

    /*
     * Handle multiple wells.
     *
     * The same kind of remark apply: We assume that a channel setting can
     * be applied to ALL wells. So this file reader will fail for dataset
     * that have one well that has a different dimension that of others.
     */

    /*
     * First remark: there can be two modes to store Areas in the xml file:
     * Either we define different areas for each well, and in that case, the
     * areas are found as a child element of the well element. Either the
     * definition of areas is common to all wells, and in that case they
     * area defined in a separate element.
     */

    final boolean sameAreaPerWell = Boolean.parseBoolean( getChildText( msRoot, "UsesSameAreaParWell" ) );
    List< AreaInfo > areas = null;
    if ( sameAreaPerWell )
    {
      final Element areasEl = getChild( msRoot, new String[] { "SameAreaUsingWell", "Areas" } );
      final List< Element > areaEls = getChildren( areasEl, "Area" );
      int areaIndex = 0;
      areas = new ArrayList< AreaInfo >( areaEls.size() );
      int fieldIndex = 1;
      for ( final Element areaEl : areaEls )
      {
        final AreaInfo area = readArea( areaEl, fieldIndex, pixelWidth, pixelHeight, tileWidth, tileHeight );
        area.index = areaIndex++;
        areas.add( area );

        // Continue incrementing field index across areas.
        fieldIndex = area.fields.get( area.fields.size() - 1 ).index + 1;
      }
    }

    final Element wellsEl = getChild( msRoot, "Wells" );
    final List< Element > wellEls = getChildren( wellsEl, "Well" );
    wells = new ArrayList< WellInfo >();
    for ( final Element wellEl : wellEls )
    {
      final boolean isWellEnabled = Boolean.parseBoolean( getChild( wellEl, "IsEnabled" ).getTextContent() );
      if ( isWellEnabled )
      {
        final WellInfo wi = readWellInfo( wellEl, pixelWidth, pixelHeight, tileWidth, tileHeight );
        if ( sameAreaPerWell )
        {
          wi.areas = areas;
        }

        wells.add( wi );
      }
    }

    /*
     * Z range.
     *
     * In this file format, the Z range appears to be general: it applies to
     * all fields of all wells.
     */

    final int nZSlices = Integer.parseInt( getChildText( msRoot, new String[] { "ZStackConditions", "NumberOfSlices" } ) );

    /*
     * Time points. They are general as well. Which just makes sense.
     */

    timePoints = readTimePoints( msDocument );

    /*
     * Populate CORE metadata for each area.
     *
     * This reader takes to convention that state that 1 area = 1 series. So
     * if you have 10 wells with 2 areas in each well, and each area is made
     * of 20 fields, you will get 20 series, and each series will be
     * stitched from 20 fields.
     */

    OMEXMLMetadataRoot root = (OMEXMLMetadataRoot) omeMD.getRoot();
    Image firstImage = root.getImage(0);

    core.clear();
    for ( final WellInfo well : wells )
    {
      for ( final AreaInfo area : well.areas )
      {
        final CoreMetadata ms = new CoreMetadata();
        core.add( ms );
        if (core.size() > 1) {
          root.addImage(firstImage);
        }

        ms.sizeX = area.width;
        ms.sizeY = area.height;
        ms.sizeZ = nZSlices;
        ms.sizeC = channelInfos.size();
        ms.sizeT = timePoints.size();
        ms.dimensionOrder = "XYCZT";
        ms.rgb = false;
        ms.imageCount = nZSlices * channelInfos.size() * timePoints.size();

        // Bit depth.
        switch ( omeMD.getPixelsType( 0 ) )
        {
        case UINT8:
          ms.pixelType = FormatTools.UINT8;
          ms.bitsPerPixel = 8;
          break;
        case UINT16:
          ms.pixelType = FormatTools.UINT16;
          ms.bitsPerPixel = 16;
          break;
        case UINT32:
          ms.pixelType = FormatTools.UINT32;
          ms.bitsPerPixel = 32;
          break;
        default:
          throw new FormatException( "Cannot read image with pixel type = " + omeMD.getPixelsType( 0 ) );
        }

        // Determined manually on sample data. Check here is the image
        // you get is weird.
        ms.littleEndian = true;
      }
    }
    omeMD.setRoot(root);

    /*
     * Populate the MetadataStore.
     */

    final MetadataStore store = makeFilterMetadata();
    MetadataConverter.convertMetadata( omeMD, store );
    MetadataTools.populatePixels( store, this, true );

    /*
     * Pinhole disk
     */

    final double pinholeSize = Double.parseDouble( getChildText( msRoot, new String[] { "PinholeDisk", "PinholeSize_um" } ) );

    /*
     * MicroPlate specific stuff
     */

    if (plateMetadata) {

      final int nrows = Integer.parseInt( getChildText( containerEl, "RowCount" ) );
      final int ncols = Integer.parseInt( getChildText( containerEl, "ColumnCount" ) );
      store.setPlateRows( new PositiveInteger( nrows ), 0 );
      store.setPlateColumns( new PositiveInteger( ncols ), 0 );

      final String plateAcqID = MetadataTools.createLSID( "PlateAcquisition", 0, 0 );
      store.setPlateAcquisitionID( plateAcqID, 0, 0 );

      final Element dimInfoEl = getChild( msRoot, "DimensionsInfo" );
      final int maxNFields = Integer.parseInt( getChild( dimInfoEl, "F" ).getAttribute( "Max" ) );
      final PositiveInteger fieldCount = FormatTools.getMaxFieldCount( maxNFields );
      if ( fieldCount != null )
      {
        store.setPlateAcquisitionMaximumFieldCount( fieldCount, 0, 0 );
      }

      // Plate acquisition time
      final String beginTime = getChildText( msRoot, "BeginTime" );
      final String endTime = getChildText( msRoot, "EndTime" );
      store.setPlateAcquisitionStartTime( new Timestamp( beginTime ), 0, 0 );
      store.setPlateAcquisitionEndTime( new Timestamp( endTime ), 0, 0 );

      store.setPlateName(beginTime, 0);
    }
    else if (!type.equals("PreparedSlide")) {
      LOGGER.warn("Unexpected acquisition type: {}", type);
    }

    // Wells position on the plate
    int seriesIndex = -1;
    int wellIndex = -1;
    for ( final WellInfo well : wells )
    {
      wellIndex++;
      final int wellNumber = well.number;
      if (plateMetadata) {
        store.setWellID(MetadataTools.createLSID("Well", 0, wellIndex), 0, wellIndex);
        store.setWellRow( new NonNegativeInteger( well.row ), 0, wellIndex );
        store.setWellColumn( new NonNegativeInteger( well.col ), 0, wellIndex );
      }
      int areaIndex = -1;
      for ( final AreaInfo area : well.areas )
      {
        seriesIndex++;
        areaIndex++;
        String imageID = MetadataTools.createLSID("Image", seriesIndex);
        store.setImageID(imageID, seriesIndex);
        final String imageName = "Well " + wellNumber + " (UID=" + well.UID + ", r=" + well.row + ", c=" + well.col + ") - Area " + areaIndex;
        store.setImageName( imageName, seriesIndex );

        if (plateMetadata) {
          Length posX = new Length(Double.valueOf(well.centerX), UNITS.REFERENCEFRAME);
          Length posY = new Length(Double.valueOf(well.centerY), UNITS.REFERENCEFRAME);

          String wellSample = MetadataTools.createLSID("WellSample", 0, wellIndex, areaIndex);
          store.setWellSampleID(wellSample, 0, wellIndex, areaIndex);
          store.setWellSampleImageRef(imageID, 0, wellIndex, areaIndex);
          store.setWellSampleIndex( new NonNegativeInteger( area.index ), 0, wellIndex, areaIndex );
          store.setWellSamplePositionX(posX, 0, wellIndex, areaIndex);
          store.setWellSamplePositionY(posY, 0, wellIndex, areaIndex);

          store.setPlateAcquisitionWellSampleRef(wellSample, 0, 0, seriesIndex);
        }
        store.setImageInstrumentRef(MetadataTools.createLSID("Instrument", 0), seriesIndex);

        for ( int i = 0; i < channelInfos.size(); i++ )
        {
          store.setChannelPinholeSize(new Length(pinholeSize, UNITS.MICROMETER), seriesIndex, i);
          store.setChannelName( channelInfos.get( i ).name, seriesIndex, i );
          store.setChannelColor(channelInfos.get(i).color, seriesIndex, i);
        }
      }

    }

  }

  private ChannelInfo readChannel( final Element channelEl )
  {
    final ChannelInfo ci = new ChannelInfo();

    ci.isEnabled = Boolean.parseBoolean( getChildText( channelEl, "IsEnabled" ) );

    ci.channelNumber = Integer.parseInt( getChildText( channelEl, "Number" ) );

    final Element acquisitionSettings = getChild( channelEl, "AcquisitionSetting" );

    final Element cameraEl = getChild( acquisitionSettings, "Camera" );
    ci.tileWidth = Integer.parseInt( getChildText( cameraEl, "EffectiveHorizontalPixels_pixel" ) );
    ci.tileHeight = Integer.parseInt( getChildText( cameraEl, "EffectiveVerticalPixels_pixel" ) );

    ci.unmagnifiedPixelWidth = Double.parseDouble( getChildText( cameraEl, "HorizonalCellSize_um" ) );
    ci.unmagnifiedPixelHeight = Double.parseDouble( getChildText( cameraEl, "VerticalCellSize_um" ) );

    final Element colorElement = getChild( channelEl, new String[] { "ContrastEnhanceParam", "Color" } );
    final int r = Integer.parseInt( getChildText( colorElement, "R" ) );
    final int g = Integer.parseInt( getChildText( colorElement, "G" ) );
    final int b = Integer.parseInt( getChildText( colorElement, "B" ) );
    final int a = Integer.parseInt( getChildText( colorElement, "A" ) );
    final Color channelColor = new Color( r, g, b, a );
    ci.color = channelColor;

    // Build a channel name from excitation, emission and fluorophore name
    final String excitationType = getChild( channelEl, "Excitation" ).getAttribute( "xsi:type" );
    final String excitationName = getChildText( channelEl, new String[] { "Excitation", "Name", "Value" } );
    final String emissionName = getChildText( channelEl, new String[] { "Emission", "Name", "Value" } );
    String fluorophoreName = getChildText( channelEl, new String[] { "Emission", "FluorescentProbe", "Value" } );
    if ( null == fluorophoreName )
    {
      fluorophoreName = "ø";
    }
    final String channelName = "Ex: " + excitationType + "(" + excitationName + ") / Em: " + emissionName + " / Fl: " + fluorophoreName;
    ci.name = channelName;

    return ci;

  }

  private WellInfo readWellInfo( final Element wellEl, final double pixelWidth, final double pixelHeight, final int tileWidth, final int tileHeight )
  {
    final WellInfo info = new WellInfo();
    info.UID = Integer.parseInt( getChildText( wellEl, "UniqueID" ) );
    info.number = Integer.parseInt( getChildText( wellEl, "Number" ) );
    info.row = Integer.parseInt( getChildText( wellEl, "Row" ) );
    info.col = Integer.parseInt( getChildText( wellEl, "Column" ) );
    info.centerX = Double.parseDouble( getChildText( wellEl, new String[] { "CenterCoord_mm", "X" } ) );
    info.centerY = Double.parseDouble( getChildText( wellEl, new String[] { "CenterCoord_mm", "Y" } ) );

    final Element areasEl = getChild( wellEl, "Areas" );
    final List< Element > areaEls = getChildren( areasEl, "Area" );
    int areaIndex = 0;
    int fieldIndex = 1;
    for ( final Element areaEl : areaEls )
    {
      final AreaInfo area = readArea( areaEl, fieldIndex, pixelWidth, pixelHeight, tileWidth, tileHeight );
      area.index = areaIndex++;
      info.areas.add( area );

      // Continue incrementing field index across areas.
      fieldIndex = area.fields.get( area.fields.size() - 1 ).index + 1;
    }

    return info;
  }

  private AreaInfo readArea( final Element areaEl, int startingFieldIndex, final double pixelWidth, final double pixelHeight, final int tileWidth, final int tileHeight )
  {
    final AreaInfo info = new AreaInfo();
    info.UID = Integer.parseInt( getChildText( areaEl, "UniqueID" ) );

    // Read field position in um
    double xmin = Double.POSITIVE_INFINITY;
    double ymin = Double.POSITIVE_INFINITY;
    double xmax = Double.NEGATIVE_INFINITY;
    double ymax = Double.NEGATIVE_INFINITY;

    final Element fieldsEl = getChild( areaEl, "Fields" );
    final List< Element > fieldEls = getChildren( fieldsEl, "Field" );

    // Read basic info and get min & max.
    for ( final Element fieldEl : fieldEls )
    {
      final FieldInfo finfo = readField( fieldEl );
      info.fields.add( finfo );

      final double xum = finfo.x;
      if ( xum < xmin )
      {
        xmin = xum;
      }
      if ( xum > xmax )
      {
        xmax = xum;
      }

      final double yum = -finfo.y;
      if ( yum < ymin )
      {
        ymin = yum;
      }
      if ( yum > ymax )
      {
        ymax = yum;
      }
    }
    for ( final FieldInfo finfo : info.fields )
    {
      final long xpixels = Math.round( ( finfo.x - xmin ) / pixelWidth );
      /*
       * Careful! For the fields to be padded correctly, we need to invert
       * their Y position, so that it matches the pixel orientation.
       */
      final long ypixels = Math.round( ( -ymin - finfo.y ) / pixelHeight );
      finfo.xpixels = xpixels;
      finfo.ypixels = ypixels;

      /*
       * Field index.
       *
       * Now there is a complexity regarding the way fields (that is:
       * tiles in common meaning) are indexed in the 'ImageIndex.xml'
       * file. Even if for a well you have two areas made of 5 tiles each,
       * there is no indexing of the areas. The field index simply keeps
       * increasing when you go from one area to the next one, and this
       * index follows the appearance order of the 'Field' xml element in
       * the 'MeasurementResult.xml' file.
       */

      finfo.index = startingFieldIndex++;
    }

    final int width = 1 + ( int ) ( ( xmax - xmin ) / pixelWidth );
    final int height = 1 + ( int ) ( ( ymax - ymin ) / pixelHeight );
    info.width = width + tileWidth;
    info.height = height + tileHeight;

    return info;
  }

  private FieldInfo readField( final Element fieldEl )
  {
    final FieldInfo info = new FieldInfo();
    info.x = Double.parseDouble( getChildText( fieldEl, "StageX_um" ) );
    info.y = Double.parseDouble( getChildText( fieldEl, "StageY_um" ) );
    // I discarded the other info (BottomOffset & co) for I don't what to do
    // with them.
    return info;
  }

  private List< Integer > readTimePoints( final Document document )
  {
    final Element root = document.getDocumentElement();
    final int nTimePoints = Integer.parseInt( getChildText( root, new String[] { "TimelapsCondition", "Iteration" } ) );
    //
    final List< Integer > timepoints = new ArrayList< Integer >( nTimePoints );
    for ( int i = 0; i < nTimePoints; i++ )
    {
      timepoints.add(i + 1);
    }
    return timepoints;
  }

  private double readFrameInterval( final Document document )
  {
    final Element root = document.getDocumentElement();
    final double dt = Double.parseDouble( getChildText( root, new String[] { "TimelapsCondition", "Interval" } ) );
    return dt;
  }

  /*
   * INNER CLASSES
   */

  private static final class FieldInfo
  {

    public int index;

    public long ypixels;

    public long xpixels;

    public double y;

    public double x;

    @Override
    public String toString()
    {
      return "\t\tField index = " + index + "\n\t\t\tX = " + x + " µm\n\t\t\tY = " + y + " µm\n" + "\t\t\txi = " + xpixels + " pixels\n" + "\t\t\tyi = " + ypixels + " pixels\n";
    }

  }

  private static final class AreaInfo
  {

    public int index;

    public int height;

    public int width;

    public List< FieldInfo > fields = new ArrayList< FieldInfo >();

    public int UID;

    @Override
    public String toString()
    {
      final StringBuilder str = new StringBuilder();
      str.append( "\tArea ID = " + UID + '\n' );
      str.append( "\t\ttotal width = " + width + " pixels\n" );
      str.append( "\t\ttotal height = " + height + " pixels\n" );
      for ( final FieldInfo fieldInfo : fields )
      {
        str.append( fieldInfo.toString() );
      }
      return str.toString();
    }

  }

  private static final class WellInfo
  {

    public List< AreaInfo > areas = new ArrayList< AreaInfo >();

    public double centerY;

    public double centerX;

    public int col;

    public int row;

    public int number;

    public int UID;

    @Override
    public String toString()
    {
      final StringBuilder str = new StringBuilder();
      str.append( "Well ID = " + UID + '\n' );
      str.append( "\tnumber = " + number + '\n' );
      str.append( "\trow = " + row + '\n' );
      str.append( "\tcol = " + col + '\n' );
      str.append( "\tcenter X = " + centerX + " mm\n" );
      str.append( "\tcenter Y = " + centerY + " mm\n" );
      for ( final AreaInfo areaInfo : areas )
      {
        str.append( areaInfo.toString() );
      }
      return str.toString();
    }

  }

  private static final class ChannelInfo
  {

    public String name;

    public Color color;

    public int height;

    public int width;

    public boolean isEnabled;

    public double unmagnifiedPixelHeight;

    public double unmagnifiedPixelWidth;

    public int tileHeight;

    public int tileWidth;

    public int channelNumber;

    @Override
    public String toString()
    {
      final StringBuilder str = new StringBuilder();
      str.append( "Channel " + channelNumber + ": \n" );
      str.append( " - name: " + name + "\n" );
      str.append( " - isEnabled: " + isEnabled + "\n" );
      str.append( " - width: " + width + "\n" );
      str.append( " - height: " + height + "\n" );
      str.append( " - tile width: " + tileWidth + "\n" );
      str.append( " - tile height: " + tileHeight + "\n" );
      str.append( " - unmagnifiedPixelWidth: " + unmagnifiedPixelWidth + "\n" );
      str.append( " - unmagnifiedPixelHeight: " + unmagnifiedPixelHeight + "\n" );
      return str.toString();
    }

  }

  private static final Element getChild( final Element parent, final String childName )
  {
    return getChild(parent, new String[] {childName});
  }

  private static final Element getChild( final Element parent, final String[] path )
  {
    final NodeList childNodes = parent.getChildNodes();
    for (int i=0; i<childNodes.getLength(); i++) {
      final Node item = childNodes.item(i);
      if (item.getNodeName().equals(path[0])) {
        if (path.length == 1) {
          return (Element) item;
        }
        return getChild((Element) item, Arrays.copyOfRange(path, 1, path.length));
      }
    }
    return null;
  }

  private static final List< Element > getChildren( final Element parent, final String name )
  {
    final NodeList nodeList = parent.getElementsByTagName( name );
    final int nEls = nodeList.getLength();
    final List< Element > children = new ArrayList< Element >( nEls );
    for ( int i = 0; i < nEls; i++ )
    {
      children.add( ( Element ) nodeList.item( i ) );
    }
    return children;
  }

  private static final String getChildText( final Element parent, final String[] path )
  {
    Element child = getChild(parent, path);
    if (child != null) {
      return child.getTextContent();
    }
    return null;
  }

  private static final String getChildText( final Element parent, final String childName )
  {
    return getChildText(parent, new String[] {childName});
  }

}
