/*
 * Copyright 2010-2013 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy 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 3 of the License, or
 * (at your option) any later version.
 * 
 * Icy 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 Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.sequence;

import icy.image.IcyBufferedImage;
import icy.type.DataType;
import icy.util.OMEUtil;
import icy.util.StringUtil;
import icy.util.XMLUtil;

import java.awt.Color;
import java.util.ArrayList;
import java.util.List;

import loci.common.services.ServiceException;
import loci.formats.MetadataTools;
import loci.formats.meta.MetadataRetrieve;
import loci.formats.ome.OMEXMLMetadata;
import loci.formats.ome.OMEXMLMetadataImpl;
import ome.xml.model.Channel;
import ome.xml.model.Dataset;
import ome.xml.model.Experiment;
import ome.xml.model.Experimenter;
import ome.xml.model.ExperimenterGroup;
import ome.xml.model.Image;
import ome.xml.model.Instrument;
import ome.xml.model.OME;
import ome.xml.model.Pixels;
import ome.xml.model.ROI;
import ome.xml.model.StructuredAnnotations;
import ome.xml.model.XMLAnnotation;
import ome.xml.model.enums.DimensionOrder;
import ome.xml.model.primitives.PositiveInteger;

/**
 * Meta data utilities class.<br>
 * Basically provide safe access to metadata.
 * 
 * @author Stephane
 */
public class MetaDataUtil
{
    public static final String DEFAULT_CHANNEL_NAME = "ch ";

    /**
     * Returns OME root element (create it if needed).
     */
    public static OME getOME(OMEXMLMetadataImpl metaData)
    {
        OME result = (OME) metaData.getRoot();

        if (result == null)
        {
            metaData.createRoot();
            result = (OME) metaData.getRoot();
        }

        return result;
    }

    /**
     * Returns the number of image serie of the specified metaData description.
     */
    public static int getNumSerie(OMEXMLMetadataImpl metaData)
    {
        return metaData.getImageCount();
    }

    /**
     * Return image serie object at specified index for the specified metaData description.
     */
    public static Image getSerie(OMEXMLMetadataImpl metaData, int index)
    {
        final OME ome = getOME(metaData);

        if (index < ome.sizeOfImageList())
            return ome.getImage(index);

        return null;
    }

    /**
     * Ensure the image serie at specified index exist for the specified metaData description.
     */
    public static Image ensureSerie(OME ome, int index)
    {
        // create missing image
        while (ome.sizeOfImageList() <= index)
        {
            final Image img = new Image();
            // create default pixels object
            img.setPixels(new Pixels());
            ome.addImage(img);
        }

        return ome.getImage(index);
    }

    /**
     * Set the number of image serie for the specified metaData description.
     */
    public static void setNumSerie(OMEXMLMetadataImpl metaData, int num)
    {
        final OME ome = getOME(metaData);

        // keep only desired number of image
        while (ome.sizeOfImageList() > num)
            ome.removeImage(ome.getImage(ome.sizeOfImageList() - 1));

        // create missing image
        ensureSerie(ome, num - 1);
    }

    /**
     * Return pixels object at specified index for the specified metaData description.
     */
    public static Pixels getPixels(OMEXMLMetadataImpl metaData, int index)
    {
        final OME ome = getOME(metaData);

        if (index < ome.sizeOfImageList())
            return ome.getImage(index).getPixels();

        return null;
    }

    /**
     * Ensure the pixels at specified index exist for the specified metaData description.
     */
    public static Pixels ensurePixels(OME ome, int index)
    {
        Image img;
        Pixels result;

        // create missing image
        while (ome.sizeOfImageList() <= index)
        {
            img = new Image();
            ome.addImage(img);
        }

        img = ome.getImage(index);
        result = img.getPixels();

        // create Pixels object
        if (result == null)
        {
            result = new Pixels();
            img.setPixels(result);
        }

        return result;
    }

    /**
     * Returns the data type of the specified image serie.
     */
    public static DataType getDataType(OMEXMLMetadataImpl metaData, int serie)
    {
        final Pixels pix = getPixels(metaData, serie);

        if (pix != null)
            return DataType.getDataTypeFromPixelType(pix.getType());

        return DataType.UNDEFINED;
    }

    /**
     * Returns the width (sizeX) of the specified image serie.
     */
    public static int getSizeX(OMEXMLMetadataImpl metaData, int serie)
    {
        final Pixels pix = getPixels(metaData, serie);

        if (pix != null)
            return OMEUtil.getValue(pix.getSizeX(), 0);

        return 0;
    }

    /**
     * Returns the height (sizeY) of the specified image serie.
     */
    public static int getSizeY(OMEXMLMetadataImpl metaData, int serie)
    {
        final Pixels pix = getPixels(metaData, serie);

        if (pix != null)
            return OMEUtil.getValue(pix.getSizeY(), 0);

        return 0;
    }

    /**
     * Returns the number of channel (sizeC) of the specified image serie.
     */
    public static int getSizeC(OMEXMLMetadataImpl metaData, int serie)
    {
        final Pixels pix = getPixels(metaData, serie);

        if (pix != null)
            return OMEUtil.getValue(pix.getSizeC(), 0);

        return 0;
    }

    /**
     * Returns the depth (sizeZ) of the specified image serie.
     */
    public static int getSizeZ(OMEXMLMetadataImpl metaData, int serie)
    {
        final Pixels pix = getPixels(metaData, serie);

        if (pix != null)
            return OMEUtil.getValue(pix.getSizeZ(), 0);

        return 0;
    }

    /**
     * Returns the number of frame (sizeT) of the specified image serie.
     */
    public static int getSizeT(OMEXMLMetadataImpl metaData, int serie)
    {
        final Pixels pix = getPixels(metaData, serie);

        if (pix != null)
            return OMEUtil.getValue(pix.getSizeT(), 0);

        return 0;
    }

    /**
     * Sets the data type of the specified image serie.
     */
    public static void setDataType(OMEXMLMetadataImpl metaData, int serie, DataType dataType)
    {
        metaData.setPixelsType(dataType.toPixelType(), serie);
    }

    /**
     * Sets the width (sizeX) of the specified image serie.
     */
    public static void setSizeX(OMEXMLMetadataImpl metaData, int serie, int sizeX)
    {
        metaData.setPixelsSizeX(OMEUtil.getPositiveInteger(sizeX), serie);
    }

    /**
     * Sets the height (sizeY) of the specified image serie.
     */
    public static void setSizeY(OMEXMLMetadataImpl metaData, int serie, int sizeY)
    {
        metaData.setPixelsSizeY(OMEUtil.getPositiveInteger(sizeY), serie);
    }

    /**
     * Sets the number of channel (sizeC) of the specified image serie.
     */
    public static void setSizeC(OMEXMLMetadataImpl metaData, int serie, int sizeC)
    {
        metaData.setPixelsSizeC(OMEUtil.getPositiveInteger(sizeC), serie);
    }

    /**
     * Sets the depth (sizeZ) of the specified image serie.
     */
    public static void setSizeZ(OMEXMLMetadataImpl metaData, int serie, int sizeZ)
    {
        metaData.setPixelsSizeZ(OMEUtil.getPositiveInteger(sizeZ), serie);
    }

    /**
     * Sets the number of frame (sizeT) of the specified image serie.
     */
    public static void setSizeT(OMEXMLMetadataImpl metaData, int serie, int sizeT)
    {
        metaData.setPixelsSizeT(OMEUtil.getPositiveInteger(sizeT), serie);
    }

    /**
     * Returns the id of the specified image serie.
     */
    public static String getImageID(OMEXMLMetadataImpl metaData, int serie)
    {
        final Image img = getSerie(metaData, serie);

        if (img != null)
            return StringUtil.getValue(img.getID(), "");

        return "";
    }

    /**
     * Set the id of the specified image serie.
     */
    public static void setImageID(OMEXMLMetadataImpl metaData, int serie, String value)
    {
        metaData.setImageID(value, serie);
    }

    /**
     * Returns the name of the specified image serie.
     */
    public static String getName(OMEXMLMetadataImpl metaData, int serie)
    {
        final Image img = getSerie(metaData, serie);

        if (img != null)
            return StringUtil.getValue(img.getName(), "");

        return "";
    }

    /**
     * Set the name of the specified image serie.
     */
    public static void setName(OMEXMLMetadataImpl metaData, int serie, String value)
    {
        metaData.setImageName(value, serie);
    }

    /**
     * Returns X pixel size (in µm) of the specified image serie.
     */
    public static double getPixelSizeX(OMEXMLMetadataImpl metaData, int serie, double defaultValue)
    {
        final Pixels pix = getPixels(metaData, serie);

        if (pix != null)
            return OMEUtil.getValue(pix.getPhysicalSizeX(), defaultValue);

        return 1d;
    }

    /**
     * Returns Y pixel size (in µm) of the specified image serie.
     */
    public static double getPixelSizeY(OMEXMLMetadataImpl metaData, int serie, double defaultValue)
    {
        final Pixels pix = getPixels(metaData, serie);

        if (pix != null)
            return OMEUtil.getValue(pix.getPhysicalSizeY(), defaultValue);

        return 1d;
    }

    /**
     * Returns Z pixel size (in µm) of the specified image serie.
     */
    public static double getPixelSizeZ(OMEXMLMetadataImpl metaData, int serie, double defaultValue)
    {
        final Pixels pix = getPixels(metaData, serie);

        if (pix != null)
            return OMEUtil.getValue(pix.getPhysicalSizeZ(), defaultValue);

        return 1d;
    }

    /**
     * Returns T time size (in second) of the specified image serie.
     */
    public static double getTimeInterval(OMEXMLMetadataImpl metaData, int serie, double defaultValue)
    {
        final Pixels pix = getPixels(metaData, serie);

        if (pix != null)
            return OMEUtil.getValue(pix.getTimeIncrement(), defaultValue);

        return 1d;
    }

    /**
     * Set X pixel size (in µm) of the specified image serie.
     */
    public static void setPixelSizeX(OMEXMLMetadataImpl metaData, int serie, double value)
    {
        metaData.setPixelsPhysicalSizeX(OMEUtil.getLength(value), serie);
    }

    /**
     * Set Y pixel size (in µm) of the specified image serie.
     */
    public static void setPixelSizeY(OMEXMLMetadataImpl metaData, int serie, double value)
    {
        metaData.setPixelsPhysicalSizeY(OMEUtil.getLength(value), serie);
    }

    /**
     * Set Z pixel size (in µm) of the specified image serie.
     */
    public static void setPixelSizeZ(OMEXMLMetadataImpl metaData, int serie, double value)
    {
        metaData.setPixelsPhysicalSizeZ(OMEUtil.getLength(value), serie);
    }

    /**
     * Set T time resolution (in second) of the specified image serie.
     */
    public static void setTimeInterval(OMEXMLMetadataImpl metaData, int serie, double value)
    {
        metaData.setPixelsTimeIncrement(OMEUtil.getTime(value), serie);
    }

    /**
     * Get default name for specified channel.
     */
    public static String getDefaultChannelName(int channel)
    {
        return DEFAULT_CHANNEL_NAME + channel;
    }

    /**
     * Returns the number of channel for the specified image serie in metaData description.
     */
    public static int getNumChannel(OMEXMLMetadataImpl metaData, int serie)
    {
        final Pixels pix = getPixels(metaData, serie);

        if (pix != null)
            return pix.sizeOfChannelList();

        return 0;
    }

    /**
     * Return channel object at specified index for the specified image serie.
     */
    public static Channel getChannel(OMEXMLMetadataImpl metaData, int serie, int index)
    {
        final Pixels pix = getPixels(metaData, serie);

        if (pix != null)
            return pix.getChannel(index);

        return null;
    }

    /**
     * Ensure the channel at specified index exist for the specified image serie.
     */
    public static Channel ensureChannel(OMEXMLMetadataImpl metaData, int serie, int index)
    {
        final Pixels pix = getPixels(metaData, serie);

        if (pix != null)
            return ensureChannel(pix, index);

        return null;
    }

    /**
     * Ensure the channel at specified index exist for the specified image serie.
     */
    public static Channel ensureChannel(Pixels pix, int index)
    {
        // create missing channel
        while (pix.sizeOfChannelList() <= index)
            pix.addChannel(new Channel());

        return pix.getChannel(index);
    }

    /**
     * Remove a channel for the specified image serie.
     */
    public static void removeChannel(OMEXMLMetadataImpl metaData, int serie, int index)
    {
        final Pixels pix = getPixels(metaData, serie);

        if (pix != null)
            removeChannel(pix, index);
    }

    /**
     * Remove a channel from the specified Pixels object.
     */
    public static void removeChannel(Pixels pix, int index)
    {
        if (pix.sizeOfChannelList() > index)
            pix.removeChannel(pix.getChannel(index));
    }

    /**
     * Set the number of channel for the specified image serie in metaData description.<br>
     * This is different from {@link #getSizeC(OMEXMLMetadataImpl, int)}.
     */
    public static void setNumChannel(OMEXMLMetadataImpl metaData, int serie, int num)
    {
        final OME ome = getOME(metaData);

        ensureSerie(ome, serie);

        final Image img = ome.getImage(serie);
        Pixels pix = img.getPixels();

        if (pix == null)
        {
            // create pixels object
            pix = new Pixels();
            img.setPixels(pix);
        }

        // keep only desired number of image
        while (pix.sizeOfChannelList() > num)
            removeChannel(pix, pix.sizeOfChannelList() - 1);

        // create missing image
        ensureChannel(pix, num - 1);
    }

    /**
     * Initialize default channel name until specified index if they are missing from the meta data
     * description.
     */
    private static void prepareMetaChannelName(OMEXMLMetadataImpl metaData, int serie, int channel)
    {
        int c = getNumChannel(metaData, serie);

        while (channel >= c)
        {
            // set default channel name
            metaData.setChannelName(getDefaultChannelName(c), serie, c);
            c++;
        }
    }

    /**
     * Returns name of specified channel image serie.
     */
    public static String getChannelName(OMEXMLMetadataImpl metaData, int serie, int channel)
    {
        // needed as LOCI does not initialize them on read
        prepareMetaChannelName(metaData, serie, channel);

        final String result = StringUtil.getValue(metaData.getChannelName(serie, channel),
                getDefaultChannelName(channel));
        final String cleaned = XMLUtil.filterString(result);

        // cleaned string != original value --> set it
        if (!cleaned.equals(result))
            setChannelName(metaData, serie, channel, cleaned);

        return cleaned;
    }

    /**
     * Set name of specified channel image serie.
     */
    public static void setChannelName(OMEXMLMetadataImpl metaData, int serie, int channel, String value)
    {
        // needed as LOCI only add current channel if it's missing
        prepareMetaChannelName(metaData, serie, channel - 1);

        metaData.setChannelName(value, serie, channel);
    }

    /**
     * Returns Color of specified channel image serie.
     */
    public static Color getChannelColor(OMEXMLMetadataImpl metaData, int serie, int channel)
    {
        // needed as LOCI does not initialize them on read
        prepareMetaChannelName(metaData, serie, channel);

        return OMEUtil.getJavaColor(metaData.getChannelColor(serie, channel));
    }

    /**
     * Create and return a default OME Metadata object with default image name.
     */
    public static OMEXMLMetadataImpl createDefaultMetadata(String name)
    {
        final OMEXMLMetadataImpl result = OMEUtil.createOMEMetadata();

        result.createRoot();
        result.setImageID(MetadataTools.createLSID("Image", 0), 0);
        result.setImageName(name, 0);

        return result;
    }

    /**
     * @deprecated Use {@link OMEUtil#createOMEMetadata(MetadataRetrieve, int)}
     */
    @Deprecated
    public static OMEXMLMetadataImpl createOMEMetadata(MetadataRetrieve metadata, int serie)
    {
        return OMEUtil.createOMEMetadata(metadata, serie);
    }

    /**
     * Set metadata object with the given image properties.
     * 
     * @param metadata
     *        metadata object to fill.
     * @param sizeX
     *        width in pixels.
     * @param sizeY
     *        height in pixels.
     * @param sizeC
     *        number of channel.
     * @param sizeZ
     *        number of Z slices.
     * @param sizeT
     *        number of T frames.
     * @param dataType
     *        data type.
     * @param separateChannel
     *        true if we want channel data to be separated.
     * @throws ServiceException
     */
    public static void setMetaData(OMEXMLMetadataImpl metadata, int sizeX, int sizeY, int sizeC, int sizeZ, int sizeT,
            DataType dataType, boolean separateChannel) throws ServiceException
    {
        OME ome = (OME) metadata.getRoot();

        if (ome == null)
        {
            metadata.createRoot();
            ome = (OME) metadata.getRoot();
        }

        // keep only one image
        setNumSerie(metadata, 1);

        // save channel name
        final List<String> channelNames = new ArrayList<String>();
        for (int c = 0; c < sizeC; c++)
            channelNames.add(getChannelName(metadata, 0, c));

        // save pixel size and time interval informations
        final double pixelSizeX = MetaDataUtil.getPixelSizeX(metadata, 0, 1d);
        final double pixelSizeY = MetaDataUtil.getPixelSizeY(metadata, 0, 1d);
        final double pixelSizeZ = MetaDataUtil.getPixelSizeZ(metadata, 0, 1d);
        final double timeInterval = MetaDataUtil.getTimeInterval(metadata, 0, 0.1d);

        // init pixels object as we set specific size here
        ome.getImage(0).setPixels(new Pixels());

        if (StringUtil.isEmpty(metadata.getImageID(0)))
            metadata.setImageID(MetadataTools.createLSID("Image", 0), 0);
        if (StringUtil.isEmpty(metadata.getImageName(0)))
            metadata.setImageName("Sample", 0);

        metadata.setPixelsID(MetadataTools.createLSID("Pixels", 0), 0);
        // prefer big endian as JVM is big endian
        metadata.setPixelsBinDataBigEndian(Boolean.TRUE, 0, 0);
        metadata.setPixelsDimensionOrder(DimensionOrder.XYCZT, 0);
        metadata.setPixelsType(dataType.toPixelType(), 0);
        metadata.setPixelsSizeX(OMEUtil.getPositiveInteger(sizeX), 0);
        metadata.setPixelsSizeY(OMEUtil.getPositiveInteger(sizeY), 0);
        metadata.setPixelsSizeC(OMEUtil.getPositiveInteger(sizeC), 0);
        metadata.setPixelsSizeZ(OMEUtil.getPositiveInteger(sizeZ), 0);
        metadata.setPixelsSizeT(OMEUtil.getPositiveInteger(sizeT), 0);

        // restore pixel size and time interval informations
        metadata.setPixelsPhysicalSizeX(OMEUtil.getLength(pixelSizeX), 0);
        metadata.setPixelsPhysicalSizeY(OMEUtil.getLength(pixelSizeY), 0);
        metadata.setPixelsPhysicalSizeZ(OMEUtil.getLength(pixelSizeZ), 0);
        metadata.setPixelsTimeIncrement(OMEUtil.getTime(timeInterval), 0);

        if (separateChannel)
        {
            for (int c = 0; c < sizeC; c++)
            {
                metadata.setChannelID(MetadataTools.createLSID("Channel", 0, c), 0, c);
                metadata.setChannelSamplesPerPixel(new PositiveInteger(Integer.valueOf(1)), 0, c);
                metadata.setChannelName(channelNames.get(c), 0, c);
            }
        }
        else
        {
            metadata.setChannelID(MetadataTools.createLSID("Channel", 0, 0), 0, 0);
            metadata.setChannelSamplesPerPixel(new PositiveInteger(Integer.valueOf(sizeC)), 0, 0);
        }
    }

    /**
     * Generates meta data for the given image properties.
     * 
     * @param sizeX
     *        width in pixels.
     * @param sizeY
     *        height in pixels.
     * @param sizeC
     *        number of channel.
     * @param sizeZ
     *        number of Z slices.
     * @param sizeT
     *        number of T frames.
     * @param dataType
     *        data type.
     * @param separateChannel
     *        true if we want channel data to be separated.
     * @return OMEXMLMetadata
     * @throws ServiceException
     */
    public static OMEXMLMetadata generateMetaData(int sizeX, int sizeY, int sizeC, int sizeZ, int sizeT,
            DataType dataType, boolean separateChannel) throws ServiceException
    {
        final OMEXMLMetadataImpl result = createDefaultMetadata("Sample");

        setMetaData(result, sizeX, sizeY, sizeC, sizeZ, sizeT, dataType, separateChannel);

        return result;
    }

    /**
     * Generates Meta Data for the given arguments.
     * 
     * @see #setMetaData(OMEXMLMetadataImpl, int, int, int, int, int, DataType, boolean)
     */
    public static OMEXMLMetadata generateMetaData(int sizeX, int sizeY, int sizeC, DataType dataType,
            boolean separateChannel) throws ServiceException
    {
        return generateMetaData(sizeX, sizeY, sizeC, 1, 1, dataType, separateChannel);
    }

    /**
     * Generates Meta Data for the given BufferedImage.
     * 
     * @see #setMetaData(OMEXMLMetadataImpl, int, int, int, int, int, DataType, boolean)
     */
    public static OMEXMLMetadata generateMetaData(IcyBufferedImage image, boolean separateChannel)
            throws ServiceException
    {
        return generateMetaData(image.getSizeX(), image.getSizeY(), image.getSizeC(), image.getDataType_(),
                separateChannel);
    }

    /**
     * Generates Meta Data for the given Sequence and parameters.
     * 
     * @see #setMetaData(OMEXMLMetadataImpl, int, int, int, int, int, DataType, boolean)
     */
    public static OMEXMLMetadata generateMetaData(Sequence sequence, boolean useZ, boolean useT, boolean separateChannel)
            throws ServiceException
    {
        final OMEXMLMetadataImpl result = OMEUtil.createOMEMetadata(sequence.getMetadata());

        setMetaData(result, sequence.getSizeX(), sequence.getSizeY(), sequence.getSizeC(), useZ ? sequence.getSizeZ()
                : 1, useT ? sequence.getSizeT() : 1, sequence.getDataType_(), separateChannel);

        return result;
    }

    /**
     * Generates Meta Data for the given Sequence and parameters.
     * 
     * @see #setMetaData(OMEXMLMetadataImpl, int, int, int, int, int, DataType, boolean)
     */
    public static OMEXMLMetadata generateMetaData(Sequence sequence, int sizeZ, int sizeT, boolean separateChannel)
            throws ServiceException
    {
        final OMEXMLMetadataImpl result = OMEUtil.createOMEMetadata(sequence.getMetadata());

        setMetaData(result, sequence.getSizeX(), sequence.getSizeY(), sequence.getSizeC(), sizeZ, sizeT,
                sequence.getDataType_(), separateChannel);

        return result;
    }

    /**
     * Generates Meta Data for the given Sequence.
     * 
     * @see #setMetaData(OMEXMLMetadataImpl, int, int, int, int, int, DataType, boolean)
     */
    public static OMEXMLMetadata generateMetaData(Sequence sequence, boolean separateChannel) throws ServiceException
    {
        return generateMetaData(sequence, true, true, separateChannel);
    }

    /**
     * Keep only the specified image serie.
     */
    public static void keepSingleSerie(OMEXMLMetadataImpl metaData, int num)
    {
        final OME ome = getOME(metaData);
        final int numSeries = ome.sizeOfImageList();
        final Image img = getSerie(metaData, num);

        // keep only the desired image
        for (int i = ome.sizeOfImageList() - 1; i >= 0; i--)
            if (i != num)
                ome.removeImage(ome.getImage(i));

        final List<Object> toKeep = new ArrayList<Object>();

        // try to keep associated dataset only
        toKeep.clear();
        for (int i = 0; i < img.sizeOfLinkedDatasetList(); i++)
            toKeep.add(img.getLinkedDataset(i));
        if (!toKeep.isEmpty())
        {
            for (int i = ome.sizeOfDatasetList() - 1; i >= 0; i--)
            {
                final Dataset obj = ome.getDataset(i);
                if (!toKeep.contains(obj))
                    ome.removeDataset(obj);
            }
        }
        // just assume they are indirectly linked
        else if (ome.sizeOfDatasetList() == numSeries)
        {
            for (int i = numSeries - 1; i >= 0; i--)
                if (i != num)
                    ome.removeDataset(ome.getDataset(i));
        }

        // try to keep associated ROI only
        toKeep.clear();
        for (int i = 0; i < img.sizeOfLinkedROIList(); i++)
            toKeep.add(img.getLinkedROI(i));
        if (!toKeep.isEmpty())
        {
            for (int i = ome.sizeOfROIList() - 1; i >= 0; i--)
            {
                final ROI obj = ome.getROI(i);
                if (!toKeep.contains(obj))
                    ome.removeROI(obj);
            }
        }
        // just assume they are indirectly linked
        else if (ome.sizeOfROIList() == numSeries)
        {
            for (int i = numSeries - 1; i >= 0; i--)
                if (i != num)
                    ome.removeROI(ome.getROI(i));
        }

        // try to keep associated experiment only
        final Experiment exp = img.getLinkedExperiment();
        if (exp != null)
        {
            for (int i = ome.sizeOfExperimentList() - 1; i >= 0; i--)
            {
                final Experiment obj = ome.getExperiment(i);
                if (obj != exp)
                    ome.removeExperiment(obj);
            }
        }
        else if (ome.sizeOfExperimentList() == numSeries)
        {
            for (int i = numSeries - 1; i >= 0; i--)
                if (i != num)
                    ome.removeExperiment(ome.getExperiment(i));
        }

        // try to keep associated experimenter only
        final Experimenter expr = img.getLinkedExperimenter();
        if (expr != null)
        {
            for (int i = ome.sizeOfExperimenterList() - 1; i >= 0; i--)
            {
                final Experimenter obj = ome.getExperimenter(i);
                if (obj != expr)
                    ome.removeExperimenter(obj);
            }
        }
        else if (ome.sizeOfExperimenterList() == numSeries)
        {
            for (int i = numSeries - 1; i >= 0; i--)
                if (i != num)
                    ome.removeExperimenter(ome.getExperimenter(i));
        }

        // try to keep associated experimenter group only
        final ExperimenterGroup exprGroup = img.getLinkedExperimenterGroup();
        if (exprGroup != null)
        {
            for (int i = ome.sizeOfExperimenterGroupList() - 1; i >= 0; i--)
            {
                final ExperimenterGroup obj = ome.getExperimenterGroup(i);
                if (obj != exprGroup)
                    ome.removeExperimenterGroup(obj);
            }
        }
        else if (ome.sizeOfExperimenterGroupList() == numSeries)
        {
            for (int i = numSeries - 1; i >= 0; i--)
                if (i != num)
                    ome.removeExperimenterGroup(ome.getExperimenterGroup(i));
        }

        // try to keep associated instrument only
        final Instrument instr = img.getLinkedInstrument();
        if (instr != null)
        {
            for (int i = ome.sizeOfInstrumentList() - 1; i >= 0; i--)
            {
                final Instrument obj = ome.getInstrument(i);
                if (obj != instr)
                    ome.removeInstrument(obj);
            }
        }
        else if (ome.sizeOfInstrumentList() == numSeries)
        {
            for (int i = numSeries - 1; i >= 0; i--)
                if (i != num)
                    ome.removeInstrument(ome.getInstrument(i));
        }

        // others misc data to clean
        if (ome.sizeOfPlateList() == numSeries)
        {
            for (int i = numSeries - 1; i >= 0; i--)
                if (i != num)
                    ome.removePlate(ome.getPlate(i));
        }
        if (ome.sizeOfProjectList() == numSeries)
        {
            for (int i = numSeries - 1; i >= 0; i--)
                if (i != num)
                    ome.removeProject(ome.getProject(i));
        }
        if (ome.sizeOfScreenList() == numSeries)
        {
            for (int i = numSeries - 1; i >= 0; i--)
                if (i != num)
                    ome.removeScreen(ome.getScreen(i));
        }
    }

    /**
     * Cleanup the meta data (sometime we have empty data structure sitting there)
     */
    public static void clean(OMEXMLMetadataImpl metaData)
    {
        final OME ome = getOME(metaData);
        final StructuredAnnotations annotations = ome.getStructuredAnnotations();

        if (annotations != null)
        {
            for (int i = annotations.sizeOfXMLAnnotationList() - 1; i >= 0; i--)
            {
                final XMLAnnotation xmlAnnotation = annotations.getXMLAnnotation(i);

                if (isEmpty(xmlAnnotation))
                    annotations.removeXMLAnnotation(xmlAnnotation);
            }
        }
    }

    /**
     * Returns <code>true</code> if the specified XML annotation are empty.
     */
    public static boolean isEmpty(XMLAnnotation xmlAnnotation)
    {
        return StringUtil.isEmpty(xmlAnnotation.getDescription()) && StringUtil.isEmpty(xmlAnnotation.getValue());
    }

}