/*
 * Copyright 2010-2015 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 plugins.kernel.roi.roi3d;

import icy.canvas.IcyCanvas;
import icy.painter.OverlayEvent;
import icy.painter.OverlayListener;
import icy.roi.BooleanMask2D;
import icy.roi.ROI;
import icy.roi.ROI2D;
import icy.roi.ROI2D.ROI2DPainter;
import icy.roi.ROI3D;
import icy.roi.ROIEvent;
import icy.roi.ROIListener;
import icy.sequence.Sequence;
import icy.system.IcyExceptionHandler;
import icy.type.point.Point5D;
import icy.type.rectangle.Rectangle3D;
import icy.util.XMLUtil;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.Rectangle2D;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.Semaphore;

import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * Base class defining a generic 3D ROI as a stack of individual 2D ROI slices.
 * 
 * @author Alexandre Dufour
 * @author Stephane Dallongeville
 * @param <R>
 *        the type of 2D ROI for each slice of this 3D ROI
 */
public class ROI3DStack<R extends ROI2D> extends ROI3D implements ROIListener, OverlayListener, Iterable<R>
{
    /**
     * @deprecated this property does not exist anymore
     */
    @Deprecated
    public static final String PROPERTY_USECHILDCOLOR = "useChildColor";

    protected final TreeMap<Integer, R> slices = new TreeMap<Integer, R>();

    protected final Class<? extends R> roiClass;
    protected Semaphore modifyingSlice;
    protected double translateZ;

    /**
     * Creates a new 3D ROI based on the given 2D ROI type.
     */
    public ROI3DStack(Class<? extends R> roiClass)
    {
        super();

        this.roiClass = roiClass;
        modifyingSlice = new Semaphore(1);
        translateZ = 0d;
    }

    @Override
    public String getDefaultName()
    {
        return "ROI2D stack";
    }

    @Override
    protected ROIPainter createPainter()
    {
        return new ROI3DStackPainter();
    }

    /**
     * Create a new empty 2D ROI slice.
     */
    protected R createSlice()
    {
        try
        {
            return roiClass.newInstance();
        }
        catch (Exception e)
        {
            IcyExceptionHandler.showErrorMessage(e, true, true);
            return null;
        }
    }

    /**
     * Returns <code>true</code> if the ROI directly uses the 2D slice color draw property and <code>false</code> if it
     * uses the global 3D ROI color draw property.
     */
    @SuppressWarnings("unchecked")
    public boolean getUseChildColor()
    {
        return ((ROI3DStackPainter) getOverlay()).getUseChildColor();
    }

    /**
     * Set to <code>true</code> if you want to directly use the 2D slice color draw property and <code>false</code> to
     * keep the global 3D ROI color draw property.
     * 
     * @see #setColor(int, Color)
     */
    @SuppressWarnings("unchecked")
    public void setUseChildColor(boolean value)
    {
        ((ROI3DStackPainter) getOverlay()).setUseChildColor(value);
    }

    /**
     * Set the painter color for the specified ROI slice.
     * 
     * @see #setUseChildColor(boolean)
     */
    @SuppressWarnings("unchecked")
    public void setColor(int z, Color value)
    {
        ((ROI3DStackPainter) getOverlay()).setColor(z, value);
    }

    @Override
    public void setCreating(boolean value)
    {
        beginUpdate();
        try
        {
            super.setCreating(value);

            modifyingSlice.acquireUninterruptibly();
            try
            {
                for (R slice : slices.values())
                    slice.setCreating(value);
            }
            finally
            {
                modifyingSlice.release();
            }
        }
        finally
        {
            endUpdate();
        }
    }

    @Override
    public void setReadOnly(boolean value)
    {
        beginUpdate();
        try
        {
            super.setReadOnly(value);

            modifyingSlice.acquireUninterruptibly();
            try
            {
                for (R slice : slices.values())
                    slice.setReadOnly(value);
            }
            finally
            {
                modifyingSlice.release();
            }
        }
        finally
        {
            endUpdate();
        }
    }

    @Override
    public void setFocused(boolean value)
    {
        beginUpdate();
        try
        {
            super.setFocused(value);

            modifyingSlice.acquireUninterruptibly();
            try
            {
                for (R slice : slices.values())
                    slice.setFocused(value);
            }
            finally
            {
                modifyingSlice.release();
            }
        }
        finally
        {
            endUpdate();
        }
    }

    @Override
    public void setSelected(boolean value)
    {
        beginUpdate();
        try
        {
            super.setSelected(value);

            modifyingSlice.acquireUninterruptibly();
            try
            {
                for (R slice : slices.values())
                    slice.setSelected(value);
            }
            finally
            {
                modifyingSlice.release();
            }
        }
        finally
        {
            endUpdate();
        }
    }

    @Override
    public void setName(String value)
    {
        beginUpdate();
        try
        {
            super.setName(value);

            modifyingSlice.acquireUninterruptibly();
            try
            {
                for (R slice : slices.values())
                    slice.setName(value);
            }
            finally
            {
                modifyingSlice.release();
            }
        }
        finally
        {
            endUpdate();
        }
    }

    @Override
    public void setT(int value)
    {
        beginUpdate();
        try
        {
            super.setT(value);

            modifyingSlice.acquireUninterruptibly();
            try
            {
                for (R slice : slices.values())
                    slice.setT(value);
            }
            finally
            {
                modifyingSlice.release();
            }
        }
        finally
        {
            endUpdate();
        }
    }

    @Override
    public void setC(int value)
    {
        beginUpdate();
        try
        {
            super.setC(value);

            modifyingSlice.acquireUninterruptibly();
            try
            {
                for (R slice : slices.values())
                    slice.setC(value);
            }
            finally
            {
                modifyingSlice.release();
            }
        }
        finally
        {
            endUpdate();
        }
    }

    /**
     * Returns <code>true</code> if the ROI stack is empty.
     */
    @Override
    public boolean isEmpty()
    {
        return slices.isEmpty();
    }

    /**
     * @return The size of this ROI stack along Z.<br>
     *         Note that the returned value indicates the difference between upper and lower bounds
     *         of this ROI, but doesn't guarantee that all slices in-between exist ( {@link #getSlice(int)} may still
     *         return <code>null</code>.<br>
     */
    public int getSizeZ()
    {
        if (slices.isEmpty())
            return 0;

        return (slices.lastKey().intValue() - slices.firstKey().intValue()) + 1;
    }

    /**
     * Returns the ROI slice at given Z position.
     */
    public R getSlice(int z)
    {
        return slices.get(Integer.valueOf(z));
    }

    /**
     * Returns the ROI slice at given Z position.
     */
    public R getSlice(int z, boolean createIfNull)
    {
        R result = getSlice(z);

        if ((result == null) && createIfNull)
        {
            result = createSlice();
            if (result != null)
                setSlice(z, result);
        }

        return result;
    }

    /**
     * Sets the ROI slice for the given Z position.
     */
    public void setSlice(int z, R roi2d)
    {
        // nothing to do
        if (getSlice(z) == roi2d)
            return;

        // remove previous
        removeSlice(z);

        if (roi2d != null)
        {
            // set Z, T and C position
            roi2d.setZ(z);
            roi2d.setT(getT());
            roi2d.setC(getC());
            // listen events from this ROI and its overlay
            roi2d.addListener(this);
            roi2d.getOverlay().addOverlayListener(this);

            // set new slice
            slices.put(Integer.valueOf(z), roi2d);
        }

        // notify ROI changed
        roiChanged(true);
    }

    /**
     * Removes slice at the given Z position and returns it.
     */
    public R removeSlice(int z)
    {
        // remove the current slice (if any)
        final R result = slices.remove(Integer.valueOf(z));

        // remove listeners
        if (result != null)
        {
            result.removeListener(this);
            result.getOverlay().removeOverlayListener(this);

            // notify ROI changed
            roiChanged(true);
        }

        return result;
    }

    /**
     * Removes all slices.
     */
    public void clear()
    {
        // nothing to do
        if (isEmpty())
            return;

        for (R slice : slices.values())
        {
            slice.removeListener(this);
            slice.getOverlay().removeOverlayListener(this);
        }

        slices.clear();
        roiChanged(true);
    }

    /**
     * Add the specified {@link ROI3DStack} content to this ROI3DStack
     */
    public void add(ROI3DStack<R> roi) throws UnsupportedOperationException
    {
        beginUpdate();
        try
        {
            for (Entry<Integer, R> entry : roi.slices.entrySet())
                add(entry.getKey().intValue(), entry.getValue());
        }
        finally
        {
            endUpdate();
        }
    }

    /**
     * Exclusively add the specified {@link ROI3DStack} content to this ROI3DStack
     */
    public void exclusiveAdd(ROI3DStack<R> roi) throws UnsupportedOperationException
    {
        beginUpdate();
        try
        {
            for (Entry<Integer, R> entry : roi.slices.entrySet())
                exclusiveAdd(entry.getKey().intValue(), entry.getValue());
        }
        finally
        {
            endUpdate();
        }
    }

    /**
     * Process intersection of the specified {@link ROI3DStack} with this ROI3DStack.
     */
    public void intersect(ROI3DStack<R> roi) throws UnsupportedOperationException
    {
        beginUpdate();
        try
        {
            final Set<Integer> keys = roi.slices.keySet();
            final Set<Integer> toRemove = new HashSet<Integer>();

            // remove slices which are not contained
            for (Integer key : slices.keySet())
                if (!keys.contains(key))
                    toRemove.add(key);

            // do remove first
            for (Integer key : toRemove)
                removeSlice(key.intValue());

            // then process intersection
            for (Entry<Integer, R> entry : roi.slices.entrySet())
                intersect(entry.getKey().intValue(), entry.getValue());
        }
        finally
        {
            endUpdate();
        }
    }

    /**
     * Remove the specified {@link ROI3DStack} from this ROI3DStack
     */
    public void subtract(ROI3DStack<R> roi) throws UnsupportedOperationException
    {
        beginUpdate();
        try
        {
            for (Entry<Integer, R> entry : roi.slices.entrySet())
                subtract(entry.getKey().intValue(), entry.getValue());
        }
        finally
        {
            endUpdate();
        }
    }

    @Override
    public ROI add(ROI roi, boolean allowCreate) throws UnsupportedOperationException
    {
        if (roi instanceof ROI3D)
        {
            final ROI3D roi3d = (ROI3D) roi;

            // only if on same position
            if ((getT() == roi3d.getT()) && (getC() == roi3d.getC()))
            {
                if (this.getClass().isInstance(roi3d))
                {
                    add((ROI3DStack) roi3d);
                    return this;
                }
            }
        }
        else if (roiClass.isInstance(roi))
        {
            final ROI2D roi2d = (ROI2D) roi;

            // only if on same position
            if ((roi2d.getZ() != -1) && (getT() == roi2d.getT()) && (getC() == roi2d.getC()))
            {
                try
                {
                    add(roi2d.getZ(), (R) roi2d);
                    return this;
                }
                catch (UnsupportedOperationException e)
                {
                    // not supported, try generic method instead
                    return super.add(roi, allowCreate);
                }
            }
        }

        return super.add(roi, allowCreate);
    }

    @Override
    public ROI intersect(ROI roi, boolean allowCreate) throws UnsupportedOperationException
    {
        if (roi instanceof ROI3D)
        {
            final ROI3D roi3d = (ROI3D) roi;

            // only if on same position
            if ((getT() == roi3d.getT()) && (getC() == roi3d.getC()))
            {
                if (this.getClass().isInstance(roi3d))
                {
                    intersect((ROI3DStack) roi3d);
                    return this;
                }
            }
            else if (roiClass.isInstance(roi))
            {
                final ROI2D roi2d = (ROI2D) roi;

                // only if on same position
                if ((roi2d.getZ() != -1) && (getT() == roi2d.getT()) && (getC() == roi2d.getC()))
                {
                    try
                    {
                        intersect(roi2d.getZ(), (R) roi2d);
                        return this;
                    }
                    catch (UnsupportedOperationException e)
                    {
                        // not supported, try generic method instead
                        return super.intersect(roi, allowCreate);
                    }
                }
            }
        }

        return super.intersect(roi, allowCreate);
    }

    @Override
    public ROI exclusiveAdd(ROI roi, boolean allowCreate) throws UnsupportedOperationException
    {
        if (roi instanceof ROI3D)
        {
            final ROI3D roi3d = (ROI3D) roi;

            // only if on same position
            if ((getT() == roi3d.getT()) && (getC() == roi3d.getC()))
            {
                if (this.getClass().isInstance(roi3d))
                {
                    exclusiveAdd((ROI3DStack) roi3d);
                    return this;
                }
            }
            else if (roiClass.isInstance(roi))
            {
                final ROI2D roi2d = (ROI2D) roi;

                // only if on same position
                if ((roi2d.getZ() != -1) && (getT() == roi2d.getT()) && (getC() == roi2d.getC()))
                {
                    try
                    {
                        exclusiveAdd(roi2d.getZ(), (R) roi2d);
                        return this;
                    }
                    catch (UnsupportedOperationException e)
                    {
                        // not supported, try generic method instead
                        return super.add(roi, allowCreate);
                    }
                }
            }
        }

        return super.add(roi, allowCreate);
    }

    @Override
    public ROI subtract(ROI roi, boolean allowCreate) throws UnsupportedOperationException
    {
        if (roi instanceof ROI3D)
        {
            final ROI3D roi3d = (ROI3D) roi;

            // only if on same position
            if ((getT() == roi3d.getT()) && (getC() == roi3d.getC()))
            {
                if (this.getClass().isInstance(roi3d))
                {
                    subtract((ROI3DStack<R>) roi3d);
                    return this;
                }
            }
            else if (roiClass.isInstance(roi))
            {
                final ROI2D roi2d = (ROI2D) roi;

                // only if on same position
                if ((roi2d.getZ() != -1) && (getT() == roi2d.getT()) && (getC() == roi2d.getC()))
                {
                    try
                    {
                        subtract(roi2d.getZ(), (R) roi2d);
                        return this;
                    }
                    catch (UnsupportedOperationException e)
                    {
                        // not supported, try generic method instead
                        return super.subtract(roi, allowCreate);
                    }
                }
            }
        }

        return super.subtract(roi, allowCreate);
    }

    /**
     * Adds content of specified <code>ROI</code> slice into the <code>ROI</code> slice at given Z position.
     * The resulting content of this <code>ROI</code> will include the union of both ROI's contents.<br>
     * If no slice was present at the specified Z position then the method is equivalent to
     * {@link #setSlice(int, ROI2D)}
     * 
     * @param z
     *        the position where the slice must be merged
     * @param roiSlice
     *        the 2D ROI to merge
     * @throws UnsupportedOperationException
     *         if the given ROI slice cannot be added to this ROI
     */
    public void add(int z, R roiSlice)
    {
        if (roiSlice == null)
            return;

        final R currentSlice = getSlice(z);
        final ROI newSlice;

        // merge both slice
        if (currentSlice != null)
        {
            // we need to modify the Z, T and C position so we do the merge correctly
            roiSlice.setZ(z);
            roiSlice.setT(getT());
            roiSlice.setC(getC());
            // do ROI union
            newSlice = currentSlice.add(roiSlice, true);

            // check the resulting ROI is the same type
            if (!newSlice.getClass().isInstance(currentSlice))
                throw new UnsupportedOperationException("Can't add the result of the merge operation on 2D slice " + z
                        + ": " + newSlice.getClassName());
        }
        else
            // get a copy
            newSlice = roiSlice.getCopy();

        // set slice
        setSlice(z, (R) newSlice);
    }

    /**
     * Sets the content of the <code>ROI</code> slice at given Z position to be the union of its current content and the
     * content of the specified <code>ROI</code>, minus their intersection.
     * The resulting <code>ROI</code> will include only content that were contained in either this <code>ROI</code> or
     * in the specified <code>ROI</code>, but not in both.<br>
     * If no slice was present at the specified Z position then the method is equivalent to
     * {@link #setSlice(int, ROI2D)}
     * 
     * @param z
     *        the position where the slice must be merged
     * @param roiSlice
     *        the 2D ROI to merge
     * @throws UnsupportedOperationException
     *         if the given ROI slice cannot be exclusively added to this ROI
     */
    public void exclusiveAdd(int z, R roiSlice)
    {
        if (roiSlice == null)
            return;

        final R currentSlice = getSlice(z);
        final ROI newSlice;

        // merge both slice
        if (currentSlice != null)
        {
            // we need to modify the Z, T and C position so we do the merge correctly
            roiSlice.setZ(z);
            roiSlice.setT(getT());
            roiSlice.setC(getC());
            // do ROI exclusive union
            newSlice = currentSlice.exclusiveAdd(roiSlice, true);

            // check the resulting ROI is same type
            if (!newSlice.getClass().isInstance(currentSlice))
                throw new UnsupportedOperationException("Can't add the result of the merge operation on 2D slice " + z
                        + ": " + newSlice.getClassName());
        }
        else
            // get a copy
            newSlice = roiSlice.getCopy();

        if (newSlice.isEmpty())
            removeSlice(z);
        else
            setSlice(z, (R) newSlice);
    }

    /**
     * Sets the content of the <code>ROI</code> slice at given Z position to the intersection of
     * its current content and the content of the specified <code>ROI</code>.
     * The resulting ROI will include only contents that were contained in both ROI.<br>
     * If no slice was present at the specified Z position then the method does nothing.
     * 
     * @param z
     *        the position where the slice must be merged
     * @param roiSlice
     *        the 2D ROI to merge
     * @throws UnsupportedOperationException
     *         if the given ROI slice cannot be intersected with this ROI
     */
    public void intersect(int z, R roiSlice)
    {
        // better to throw an exception here than removing slice
        if (roiSlice == null)
            throw new IllegalArgumentException("Cannot intersect an empty slice in a 3D ROI");

        final R currentSlice = getSlice(z);

        // merge both slice
        if (currentSlice != null)
        {
            // we need to modify the Z, T and C position so we do the merge correctly
            roiSlice.setZ(z);
            roiSlice.setT(getT());
            roiSlice.setC(getC());
            // do ROI intersection
            final ROI newSlice = currentSlice.intersect(roiSlice, true);

            // check the resulting ROI is same type
            if (!newSlice.getClass().isInstance(currentSlice))
                throw new UnsupportedOperationException("Can't add the result of the merge operation on 2D slice " + z
                        + ": " + newSlice.getClassName());

            if (newSlice.isEmpty())
                removeSlice(z);
            else
                setSlice(z, (R) newSlice);
        }
    }

    /**
     * Subtract the specified <code>ROI</code> content from the <code>ROI</code> slice at given Z position.<br>
     * If no slice was present at the specified Z position then the method does nothing.
     * 
     * @param z
     *        the position where the subtraction should be done
     * @param roiSlice
     *        the 2D ROI to subtract from Z slice
     * @throws UnsupportedOperationException
     *         if the given ROI slice cannot be subtracted from this ROI
     */
    public void subtract(int z, R roiSlice) throws UnsupportedOperationException
    {
        if (roiSlice == null)
            return;

        final R currentSlice = getSlice(z);

        // merge both slice
        if (currentSlice != null)
        {
            // we need to modify the Z, T and C position so we do the merge correctly
            roiSlice.setZ(z);
            roiSlice.setT(getT());
            roiSlice.setC(getC());
            // do ROI subtraction
            final ROI newSlice = currentSlice.subtract(roiSlice, true);

            // check the resulting ROI is same type
            if (!newSlice.getClass().isInstance(currentSlice))
                throw new UnsupportedOperationException("Can't add the result of the merge operation on 2D slice " + z
                        + ": " + newSlice.getClassName());

            if (newSlice.isEmpty())
                removeSlice(z);
            else
                setSlice(z, (R) newSlice);
        }
    }

    /**
     * Called when a ROI slice has changed.
     */
    protected void sliceChanged(ROIEvent event)
    {
        if (modifyingSlice.availablePermits() <= 0)
            return;

        final ROI source = event.getSource();

        switch (event.getType())
        {
            case ROI_CHANGED:
                // position change of a slice can change global bounds --> transform to 'content changed' event type
                roiChanged(true);
                // roiChanged(StringUtil.equals(event.getPropertyName(), ROI_CHANGED_ALL));
                break;

            case FOCUS_CHANGED:
                setFocused(source.isFocused());
                break;

            case SELECTION_CHANGED:
                setSelected(source.isSelected());
                break;

            case PROPERTY_CHANGED:
                final String propertyName = event.getPropertyName();

                if ((propertyName == null) || propertyName.equals(PROPERTY_READONLY))
                    setReadOnly(source.isReadOnly());
                if ((propertyName == null) || propertyName.equals(PROPERTY_CREATING))
                    setCreating(source.isCreating());
                break;
        }
    }

    /**
     * Called when a ROI slice overlay has changed.
     */
    protected void sliceOverlayChanged(OverlayEvent event)
    {
        switch (event.getType())
        {
            case PAINTER_CHANGED:
                // forward the event to ROI stack overlay
                getOverlay().painterChanged();
                break;

            case PROPERTY_CHANGED:
                // forward the event to ROI stack overlay
                getOverlay().propertyChanged(event.getPropertyName());
                break;
        }
    }

    @Override
    public Rectangle3D computeBounds3D()
    {
        Rectangle2D xyBounds = null;

        for (R slice : slices.values())
        {
            final Rectangle2D bnd2d = slice.getBounds2D();

            // only add non empty bounds
            if (!bnd2d.isEmpty())
            {
                if (xyBounds == null)
                    xyBounds = (Rectangle2D) bnd2d.clone();
                else
                    xyBounds.add(bnd2d);
            }
        }

        // create empty 2D bounds
        if (xyBounds == null)
            xyBounds = new Rectangle2D.Double();

        final int z;
        final int sizeZ;

        if (!slices.isEmpty())
        {
            z = slices.firstKey().intValue();
            sizeZ = getSizeZ();
        }
        else
        {
            z = 0;
            sizeZ = 0;
        }

        return new Rectangle3D.Double(xyBounds.getX(), xyBounds.getY(), z, xyBounds.getWidth(), xyBounds.getHeight(),
                sizeZ);
    }

    @Override
    public boolean contains(double x, double y, double z)
    {
        final R roi2d = getSlice((int) Math.floor(z));

        if (roi2d != null)
            return roi2d.contains(x, y);

        return false;
    }

    @Override
    public boolean contains(double x, double y, double z, double sizeX, double sizeY, double sizeZ)
    {
        final Rectangle3D bounds = getBounds3D();

        // easy discard
        if (!bounds.contains(x, y, z, sizeX, sizeY, sizeZ))
            return false;

        final int lim = (int) Math.floor(z + sizeZ);
        for (int zc = (int) Math.floor(z); zc < lim; zc++)
        {
            final R roi2d = getSlice(zc);
            if ((roi2d == null) || !roi2d.contains(x, y, sizeX, sizeY))
                return false;
        }

        return true;
    }

    @Override
    public boolean intersects(double x, double y, double z, double sizeX, double sizeY, double sizeZ)
    {
        final Rectangle3D bounds = getBounds3D();

        // easy discard
        if (!bounds.intersects(x, y, z, sizeX, sizeY, sizeZ))
            return false;

        final int lim = (int) Math.floor(z + sizeZ);
        for (int zc = (int) Math.floor(z); zc < lim; zc++)
        {
            final R roi2d = getSlice(zc);
            if ((roi2d != null) && roi2d.intersects(x, y, sizeX, sizeY))
                return true;
        }

        return false;
    }

    @Override
    public boolean hasSelectedPoint()
    {
        // default
        return false;
    }

    @Override
    public void unselectAllPoints()
    {
        beginUpdate();
        try
        {
            modifyingSlice.acquireUninterruptibly();
            try
            {
                for (R slice : slices.values())
                    slice.unselectAllPoints();
            }
            finally
            {
                modifyingSlice.release();
            }
        }
        finally
        {
            endUpdate();
        }
    }

    // default approximated implementation for ROI3DStack
    @Override
    public double computeSurfaceArea(Sequence sequence) throws UnsupportedOperationException
    {
        // 3D contour points = first slice points + all slices perimeter + last slice points
        double result = 0;

        if (!slices.isEmpty())
        {
            final double psx = sequence.getPixelSizeX();
            final double psy = sequence.getPixelSizeY();
            final double psz = sequence.getPixelSizeZ();

            result = slices.firstEntry().getValue().getNumberOfPoints() * psx * psy;
            result += slices.lastEntry().getValue().getNumberOfPoints() * psx * psy;

            for (R slice : slices.values())
                result += slice.getLength(sequence) * psz;
        }

        return result;
    }

    // default approximated implementation for ROI3DStack
    @Override
    public double computeNumberOfContourPoints()
    {
        // 3D contour points = first slice points + inter slices contour points + last slice points
        double result = 0;

        if (slices.size() <= 2)
        {
            for (R slice : slices.values())
                result += slice.getNumberOfPoints();
        }
        else
        {
            final Entry<Integer, R> firstEntry = slices.firstEntry();
            final Entry<Integer, R> lastEntry = slices.lastEntry();
            final Integer firstKey = firstEntry.getKey();
            final Integer lastKey = lastEntry.getKey();

            result = firstEntry.getValue().getNumberOfPoints();

            for (R slice : slices.subMap(firstKey, false, lastKey, false).values())
                result += slice.getNumberOfContourPoints();

            result += lastEntry.getValue().getNumberOfPoints();
        }

        return result;
    }

    @Override
    public double computeNumberOfPoints()
    {
        double volume = 0;

        for (R slice : slices.values())
            volume += slice.getNumberOfPoints();

        return volume;
    }

    @Override
    public boolean canTranslate()
    {
        // only need to test the first entry
        if (!slices.isEmpty())
            return slices.firstEntry().getValue().canTranslate();

        return false;
    }

    /**
     * Translate the stack of specified Z position.
     */
    public void translate(int z)
    {
        // easy optimizations
        if ((z == 0) || isEmpty())
            return;

        final Map<Integer, R> map = new HashMap<Integer, R>(slices);

        slices.clear();
        for (Entry<Integer, R> entry : map.entrySet())
        {
            final R roi = entry.getValue();
            final int newZ = roi.getZ() + z;

//            // only positive value accepted
//            if (newZ >= 0)
//            {
                roi.setZ(newZ);
                slices.put(Integer.valueOf(newZ), roi);
//            }
        }

        // notify ROI changed
        roiChanged(false);
    }

    @Override
    public void translate(double dx, double dy, double dz)
    {
        beginUpdate();
        try
        {
            translateZ += dz;
            // convert to integer
            final int dzi = (int) translateZ;
            // keep trace of not used floating part
            translateZ -= dzi;

            translate(dzi);

            modifyingSlice.acquireUninterruptibly();
            try
            {
                for (R slice : slices.values())
                    slice.translate(dx, dy);
            }
            finally
            {
                modifyingSlice.release();
            }

            // notify ROI changed because we modified slice 'internally'
            if ((dx != 0d) || (dy != 0d))
                roiChanged(false);
        }
        finally
        {
            endUpdate();
        }
    }

    @Override
    public boolean[] getBooleanMask2D(int x, int y, int width, int height, int z, boolean inclusive)
    {
        final R roi2d = getSlice(z);

        if (roi2d != null)
            return roi2d.getBooleanMask(x, y, width, height, inclusive);

        return new boolean[width * height];
    }

    @Override
    public BooleanMask2D getBooleanMask2D(int z, boolean inclusive)
    {
        final R roi2d = getSlice(z);

        if (roi2d != null)
            return roi2d.getBooleanMask(inclusive);

        return new BooleanMask2D(new Rectangle(), new boolean[0]);
    }

    // called when one of the slice ROI changed
    @Override
    public void roiChanged(ROIEvent event)
    {
        // propagate children change event
        sliceChanged(event);
    }

    // called when one of the slice ROI overlay changed
    @Override
    public void overlayChanged(OverlayEvent event)
    {
        // propagate children overlay change event
        sliceOverlayChanged(event);
    }

    @Override
    public Iterator<R> iterator()
    {
        return slices.values().iterator();
    }

    @Override
    public boolean loadFromXML(Node node)
    {
        beginUpdate();
        try
        {
            if (!super.loadFromXML(node))
                return false;

            // we don't need to save the 2D ROI class as the parent class already do it
            clear();

            for (Element e : XMLUtil.getElements(node, "slice"))
            {
                // faster than using complete XML serialization
                final R slice = createSlice();

                // error while reloading the ROI from XML
                if ((slice == null) || !slice.loadFromXML(e))
                    return false;

                setSlice(slice.getZ(), slice);
            }
        }
        finally
        {
            endUpdate();
        }

        return true;
    }

    @Override
    public boolean saveToXML(Node node)
    {
        if (!super.saveToXML(node))
            return false;

        for (R slice : slices.values())
        {
            Element sliceNode = XMLUtil.addElement(node, "slice");

            if (!slice.saveToXML(sliceNode))
                return false;
        }

        return true;
    }

    public class ROI3DStackPainter extends ROI3DPainter
    {
        protected ROIPainter getSliceOverlayForCanvas(IcyCanvas canvas)
        {
            final int z = canvas.getPositionZ();

            // canvas position of -1 mean 3D canvas (all Z visible)
            if (z >= 0)
                return getSliceOverlay(z);

            return null;
        }

        /**
         * Returns the ROI overlay at given Z position.
         */
        protected ROIPainter getSliceOverlay(int z)
        {
            R roi = getSlice(z);

            if (roi != null)
                return roi.getOverlay();

            return null;
        }

        /**
         * @deprecated this property does not exist anymore (always return <code>false</code>)
         */
        @Deprecated
        public boolean getUseChildColor()
        {
            return false;
        }

        /**
         * @deprecated this property does not exist anymore
         */
        @Deprecated
        public void setUseChildColor(boolean value)
        {
            //
        }

        /**
         * Set the painter color for the specified ROI slice.
         * 
         * @see #setUseChildColor(boolean)
         */
        public void setColor(int z, Color value)
        {
            final ROIPainter sliceOverlay = getSliceOverlay(z);

            if (sliceOverlay != null)
            {
                modifyingSlice.acquireUninterruptibly();
                try
                {
                    sliceOverlay.setColor(value);
                }
                finally
                {
                    modifyingSlice.release();
                }
            }
        }

        @Override
        public void setColor(Color value)
        {
            beginUpdate();
            try
            {
                super.setColor(value);

                if (!getUseChildColor())
                {
                    modifyingSlice.acquireUninterruptibly();
                    try
                    {
                        for (R slice : slices.values())
                            slice.getOverlay().setColor(value);
                    }
                    finally
                    {
                        modifyingSlice.release();
                    }
                }
            }
            finally
            {
                endUpdate();
            }
        }

        @Override
        public void setOpacity(float value)
        {
            beginUpdate();
            try
            {
                super.setOpacity(value);

                modifyingSlice.acquireUninterruptibly();
                try
                {
                    for (R slice : slices.values())
                        slice.getOverlay().setOpacity(value);
                }
                finally
                {
                    modifyingSlice.release();
                }
            }
            finally
            {
                endUpdate();
            }
        }

        @Override
        public void setStroke(double value)
        {
            beginUpdate();
            try
            {
                super.setStroke(value);

                modifyingSlice.acquireUninterruptibly();
                try
                {
                    for (R slice : slices.values())
                        slice.getOverlay().setStroke(value);
                }
                finally
                {
                    modifyingSlice.release();
                }
            }
            finally
            {
                endUpdate();
            }
        }

        @Override
        public void setShowName(boolean value)
        {
            beginUpdate();
            try
            {
                super.setShowName(value);

                modifyingSlice.acquireUninterruptibly();
                try
                {
                    for (R slice : slices.values())
                        slice.getOverlay().setShowName(value);
                }
                finally
                {
                    modifyingSlice.release();
                }
            }
            finally
            {
                endUpdate();
            }
        }

        @Override
        public void paint(Graphics2D g, Sequence sequence, IcyCanvas canvas)
        {
            // 2D canvas --> use slice implementation
            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
            {
                // forward event to current slice
                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);

                if (sliceOverlay != null)
                    sliceOverlay.paint(g, sequence, canvas);
            }
            // use default parent implementation
            else
                super.paint(g, sequence, canvas);
        }

        @Override
        public void keyPressed(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // 2D canvas --> use slice implementation
            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
            {
                // forward event to current slice
                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);

                if (sliceOverlay != null)
                    sliceOverlay.keyPressed(e, imagePoint, canvas);
            }
            // use default parent implementation
            else
                super.keyPressed(e, imagePoint, canvas);
        }

        @Override
        public void keyReleased(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // 2D canvas --> use slice implementation
            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
            {
                // forward event to current slice
                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);

                if (sliceOverlay != null)
                    sliceOverlay.keyReleased(e, imagePoint, canvas);
            }
            // use default parent implementation
            else
                super.keyReleased(e, imagePoint, canvas);
        }

        @Override
        public void mouseEntered(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // 2D canvas --> use slice implementation
            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
            {
                // forward event to current slice
                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);

                if (sliceOverlay != null)
                    sliceOverlay.mouseEntered(e, imagePoint, canvas);
            }
            // use default parent implementation
            else
                super.mouseEntered(e, imagePoint, canvas);
        }

        @Override
        public void mouseExited(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // 2D canvas --> use slice implementation
            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
            {
                // forward event to current slice
                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);

                if (sliceOverlay != null)
                    sliceOverlay.mouseExited(e, imagePoint, canvas);
            }
            // use default parent implementation
            else
                super.mouseExited(e, imagePoint, canvas);
        }

        @Override
        public void mouseMove(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // 2D canvas --> use slice implementation
            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
            {
                // forward event to current slice
                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);

                if (sliceOverlay != null)
                    sliceOverlay.mouseMove(e, imagePoint, canvas);
            }
            // use default parent implementation
            else
                super.mouseMove(e, imagePoint, canvas);
        }

        @Override
        public void mouseDrag(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // 2D canvas --> use slice implementation
            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
            {
                // forward event to current slice
                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);

                if (sliceOverlay != null)
                    sliceOverlay.mouseDrag(e, imagePoint, canvas);
            }
            // use default parent implementation
            else
                super.mouseDrag(e, imagePoint, canvas);
        }

        @Override
        public void mousePressed(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // 2D canvas --> use slice implementation
            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
            {
                // forward event to current slice
                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);

                if (sliceOverlay != null)
                    sliceOverlay.mousePressed(e, imagePoint, canvas);
            }
            // use default parent implementation
            else
                super.mousePressed(e, imagePoint, canvas);
        }

        @Override
        public void mouseReleased(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // 2D canvas --> use slice implementation
            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
            {
                // forward event to current slice
                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);

                if (sliceOverlay != null)
                    sliceOverlay.mouseReleased(e, imagePoint, canvas);
            }
            // use default parent implementation
            else
                super.mouseReleased(e, imagePoint, canvas);
        }

        @Override
        public void mouseClick(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // 2D canvas --> use slice implementation
            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
            {
                // forward event to current slice
                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);

                if (sliceOverlay != null)
                    sliceOverlay.mouseClick(e, imagePoint, canvas);
            }
            // use default parent implementation
            else
                super.mouseClick(e, imagePoint, canvas);
        }

        @Override
        public void mouseWheelMoved(MouseWheelEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // 2D canvas --> use slice implementation
            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
            {
                // forward event to current slice
                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);

                if (sliceOverlay != null)
                    sliceOverlay.mouseWheelMoved(e, imagePoint, canvas);
            }
            // use default parent implementation
            else
                super.mouseWheelMoved(e, imagePoint, canvas);
        }

        @Override
        public void drawROI(Graphics2D g, Sequence sequence, IcyCanvas canvas)
        {
            // 2D canvas --> use slice implementation if possible
            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
            {
                // forward event to current slice
                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);

                if (sliceOverlay instanceof ROI2DPainter)
                    ((ROI2DPainter) sliceOverlay).drawROI(g, sequence, canvas);
            }

            // nothing to do...
        }
    }
}