/*
 * 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 java.awt.Color;
import java.awt.Graphics2D;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Set;

import icy.canvas.IcyCanvas;
import icy.common.CollapsibleEvent;
import icy.gui.inspector.RoisPanel;
import icy.main.Icy;
import icy.painter.VtkPainter;
import icy.roi.BooleanMask2D;
import icy.roi.BooleanMask3D;
import icy.roi.ROI;
import icy.roi.ROI2D;
import icy.roi.ROI3D;
import icy.roi.ROIEvent;
import icy.sequence.Sequence;
import icy.system.thread.ThreadUtil;
import icy.type.point.Point3D;
import icy.type.point.Point5D;
import icy.type.rectangle.Rectangle3D;
import icy.util.StringUtil;
import icy.vtk.IcyVtkPanel;
import icy.vtk.VtkUtil;
import plugins.kernel.canvas.VtkCanvas;
import plugins.kernel.roi.roi2d.ROI2DArea;
import vtk.vtkActor;
import vtk.vtkImageData;
import vtk.vtkInformation;
import vtk.vtkPolyData;
import vtk.vtkPolyDataMapper;
import vtk.vtkProp;

/**
 * 3D Area ROI.
 * 
 * @author Stephane
 */
public class ROI3DArea extends ROI3DStack<ROI2DArea>
{
    public class ROI3DAreaPainter extends ROI3DStackPainter implements VtkPainter, Runnable
    {
        // VTK 3D objects
        protected vtkPolyData outline;
        protected vtkPolyDataMapper outlineMapper;
        protected vtkActor outlineActor;
        protected vtkInformation vtkInfo;
        protected vtkPolyData polyData;
        protected vtkPolyDataMapper polyMapper;
        protected vtkActor surfaceActor;
        // 3D internal
        protected boolean needRebuild;
        protected double scaling[];
        protected WeakReference<VtkCanvas> canvas3d;

        public ROI3DAreaPainter()
        {
            super();

            outline = null;
            outlineMapper = null;
            outlineActor = null;
            vtkInfo = null;
            polyData = null;
            polyMapper = null;
            surfaceActor = null;

            scaling = new double[3];
            Arrays.fill(scaling, 1d);

            needRebuild = true;
            canvas3d = new WeakReference<VtkCanvas>(null);
        }

        @Override
        protected void finalize() throws Throwable
        {
            super.finalize();

            // release allocated VTK resources
            if (surfaceActor != null)
                surfaceActor.Delete();
            if (polyMapper != null)
                polyMapper.Delete();
            if (polyData != null)
            {
                polyData.GetPointData().GetScalars().Delete();
                polyData.GetPointData().Delete();
                polyData.Delete();
            }
            if (outlineActor != null)
            {
                outlineActor.SetPropertyKeys(null);
                outlineActor.Delete();
            }
            if (vtkInfo != null)
            {
                vtkInfo.Remove(VtkCanvas.visibilityKey);
                vtkInfo.Delete();
            }
            if (outlineMapper != null)
                outlineMapper.Delete();
            if (outline != null)
            {
                outline.GetPointData().GetScalars().Delete();
                outline.GetPointData().Delete();
                outline.Delete();
            }
        };

        protected void initVtkObjects()
        {
            outline = VtkUtil.getOutline(0d, 1d, 0d, 1d, 0d, 1d);
            outlineMapper = new vtkPolyDataMapper();
            outlineMapper.SetInputData(outline);
            outlineActor = new vtkActor();
            outlineActor.SetMapper(outlineMapper);
            // disable picking on the outline
            outlineActor.SetPickable(0);
            // and set it to wireframe representation
            outlineActor.GetProperty().SetRepresentationToWireframe();
            // use vtkInformations to store outline visibility state (hacky)
            vtkInfo = new vtkInformation();
            vtkInfo.Set(VtkCanvas.visibilityKey, 0);
            // VtkCanvas use this to restore correctly outline visibility flag
            outlineActor.SetPropertyKeys(vtkInfo);

            polyMapper = new vtkPolyDataMapper();
            surfaceActor = new vtkActor();
            surfaceActor.SetMapper(polyMapper);

            final Color col = getColor();
            final double r = col.getRed() / 255d;
            final double g = col.getGreen() / 255d;
            final double b = col.getBlue() / 255d;

            // set actors color
            outlineActor.GetProperty().SetColor(r, g, b);
            surfaceActor.GetProperty().SetColor(r, g, b);
        }

        /**
         * rebuild VTK objects (called only when VTK canvas is selected).
         */
        protected void rebuildVtkObjects()
        {
            final VtkCanvas canvas = canvas3d.get();
            // canvas was closed
            if (canvas == null)
                return;

            final IcyVtkPanel vtkPanel = canvas.getVtkPanel();
            // canvas was closed
            if (vtkPanel == null)
                return;

            final Sequence seq = canvas.getSequence();
            // nothing to update
            if (seq == null)
                return;

            // get previous polydata object
            final vtkPolyData previousPolyData = polyData;

            // get VTK binary image from ROI mask
            final vtkImageData imageData = VtkUtil.getBinaryImageData(ROI3DArea.this, seq.getSizeZ(),
                    canvas.getPositionT());
            // adjust spacing
            imageData.SetSpacing(scaling[0], scaling[1], scaling[2]);
            // get VTK polygon data representing the surface of the binary image
            polyData = VtkUtil.getSurfaceFromImage(imageData, 0.5d);

            // get bounds
            final Rectangle3D bounds = getBounds3D();
            // apply scaling on bounds
            bounds.setX(bounds.getX() * scaling[0]);
            bounds.setSizeX(bounds.getSizeX() * scaling[0]);
            bounds.setY(bounds.getY() * scaling[1]);
            bounds.setSizeY(bounds.getSizeY() * scaling[1]);
            if (bounds.isInfiniteZ())
            {
                bounds.setZ(0);
                bounds.setSizeZ(seq.getSizeZ() * scaling[2]);
            }
            else
            {
                bounds.setZ(bounds.getZ() * scaling[2]);
                bounds.setSizeZ(bounds.getSizeZ() * scaling[2]);
            }

            // actor can be accessed in canvas3d for rendering so we need to synchronize access
            vtkPanel.lock();
            try
            {
                // update outline data
                VtkUtil.setOutlineBounds(outline, bounds.getMinX(), bounds.getMaxX(), bounds.getMinY(),
                        bounds.getMaxY(), bounds.getMinZ(), bounds.getMaxZ(), canvas);
                outlineMapper.Update();
                // update surface polygon data
                polyMapper.SetInputData(polyData);
                polyMapper.Update();

                // update actor position
                surfaceActor.SetPosition(bounds.getX(), bounds.getY(), bounds.getZ());

                // release image data
                imageData.GetPointData().GetScalars().Delete();
                imageData.GetPointData().Delete();
                imageData.Delete();

                // release previous polydata
                if (previousPolyData != null)
                {
                    previousPolyData.GetPointData().GetScalars().Delete();
                    previousPolyData.GetPointData().Delete();
                    previousPolyData.Delete();
                }
            }
            finally
            {
                vtkPanel.unlock();
            }

            // update color and others properties
            updateVtkDisplayProperties();
        }

        protected void updateVtkDisplayProperties()
        {
            if (surfaceActor == null)
                return;

            final VtkCanvas cnv = canvas3d.get();
            final Color col = getDisplayColor();
            final double r = col.getRed() / 255d;
            final double g = col.getGreen() / 255d;
            final double b = col.getBlue() / 255d;
            // final double strk = getStroke();
            // final float opacity = getOpacity();

            final IcyVtkPanel vtkPanel = (cnv != null) ? cnv.getVtkPanel() : null;

            // we need to lock canvas as actor can be accessed during rendering
            if (vtkPanel != null)
                vtkPanel.lock();
            try
            {
                // set actors color
                outlineActor.GetProperty().SetColor(r, g, b);
                if (isSelected())
                {
                    outlineActor.GetProperty().SetRepresentationToWireframe();
                    outlineActor.SetVisibility(1);
                    vtkInfo.Set(VtkCanvas.visibilityKey, 1);
                }
                else
                {
                    outlineActor.GetProperty().SetRepresentationToPoints();
                    outlineActor.SetVisibility(0);
                    vtkInfo.Set(VtkCanvas.visibilityKey, 0);
                }
                surfaceActor.GetProperty().SetColor(r, g, b);
                // opacity here is about ROI content, global opacity is handled by Layer
                // surfaceActor.GetProperty().SetOpacity(opacity);
                setVtkObjectsColor(col);
            }
            finally
            {
                if (vtkPanel != null)
                    vtkPanel.unlock();
            }

            // need to repaint
            painterChanged();
        }

        protected void setVtkObjectsColor(Color color)
        {
            if (outline != null)
                VtkUtil.setPolyDataColor(outline, color, canvas3d.get());
            if (polyData != null)
                VtkUtil.setPolyDataColor(polyData, color, canvas3d.get());
        }

        @Override
        public void mouseClick(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // provide backward compatibility
            if (imagePoint != null)
                mouseClick(e, imagePoint.toPoint2D(), canvas);
            else
                mouseClick(e, (Point2D) null, canvas);

            // not yet consumed...
            if (!e.isConsumed())
            {
                // and process ROI stuff now
                if (isActiveFor(canvas))
                {
                    final int clickCount = e.getClickCount();

                    // double click
                    if (clickCount == 2)
                    {
                        // focused ?
                        if (isFocused())
                        {
                            // show in ROI panel
                            final RoisPanel roiPanel = Icy.getMainInterface().getRoisPanel();

                            if (roiPanel != null)
                            {
                                roiPanel.scrollTo(ROI3DArea.this);
                                // consume event
                                e.consume();
                            }
                        }
                    }
                }
            }
        }

        @Override
        public void paint(Graphics2D g, Sequence sequence, IcyCanvas canvas)
        {
            if (isActiveFor(canvas))
            {
                if (canvas instanceof VtkCanvas)
                {
                    // 3D canvas
                    final VtkCanvas cnv = (VtkCanvas) canvas;
                    // update reference if needed
                    if (canvas3d.get() != cnv)
                        canvas3d = new WeakReference<VtkCanvas>(cnv);

                    // FIXME : need a better implementation
                    final double[] s = cnv.getVolumeScale();

                    // scaling changed ?
                    if (!Arrays.equals(scaling, s))
                    {
                        // update scaling
                        scaling = s;
                        // need rebuild
                        needRebuild = true;
                    }

                    // need to rebuild 3D data structures ?
                    if (needRebuild)
                    {
                        // initialize VTK objects if not yet done
                        if (surfaceActor == null)
                            initVtkObjects();

                        // request rebuild 3D objects
                        ThreadUtil.runSingle(this);
                        needRebuild = false;
                    }
                }
                else
                    super.paint(g, sequence, canvas);
            }
        }

        @Override
        protected boolean updateFocus(InputEvent e, Point5D imagePoint, IcyCanvas canvas)
        {
            // specific VTK canvas processing
            if (canvas instanceof VtkCanvas)
            {
                // mouse is over the ROI actor ? --> focus the ROI
                final boolean focused = (surfaceActor != null)
                        && (surfaceActor == ((VtkCanvas) canvas).getPickedObject());

                setFocused(focused);

                return focused;
            }

            return super.updateFocus(e, imagePoint, canvas);
        }

        @Override
        public vtkProp[] getProps()
        {
            // initialize VTK objects if not yet done
            if (surfaceActor == null)
                initVtkObjects();

            return new vtkActor[] {surfaceActor, outlineActor};
        }

        @Override
        public void run()
        {
            rebuildVtkObjects();
        }
    }

    public ROI3DArea()
    {
        super(ROI2DArea.class);
    }

    public ROI3DArea(Point3D pt)
    {
        this();

        addBrush(pt.toPoint2D(), (int) pt.getZ());
    }

    public ROI3DArea(Point5D pt)
    {
        this(pt.toPoint3D());
    }

    /**
     * Create a 3D Area ROI type from the specified {@link BooleanMask3D}.
     */
    public ROI3DArea(BooleanMask3D mask)
    {
        this();

        setAsBooleanMask(mask);
    }

    /**
     * Create a copy of the specified 3D Area ROI.
     */
    public ROI3DArea(ROI3DArea area)
    {
        this();

        // copy the source 3D area ROI
        for (Entry<Integer, ROI2DArea> entry : area.slices.entrySet())
            slices.put(entry.getKey(), new ROI2DArea(entry.getValue()));

        roiChanged(true);
    }

    /**
     * Create a 3D Area ROI type from the specified {@link BooleanMask3D}.
     */
    public ROI3DArea(BooleanMask2D mask2d, int zMin, int zMax)
    {
        this();

        if (zMax < zMin)
            throw new IllegalArgumentException("ROI3DArea: cannot create the ROI (zMax < zMin).");

        beginUpdate();
        try
        {
            for (int z = zMin; z <= zMax; z++)
                setSlice(z, new ROI2DArea(mask2d));
        }
        finally
        {
            endUpdate();
        }
    }

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

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

    /**
     * Adds the specified point to this ROI
     */
    public void addPoint(int x, int y, int z)
    {
        setPoint(x, y, z, true);
    }

    /**
     * Remove a point from the mask.<br>
     * Don't forget to call optimizeBounds() after consecutive remove operation
     * to refresh the mask bounds.
     */
    public void removePoint(int x, int y, int z)
    {
        setPoint(x, y, z, false);
    }

    /**
     * Set the value for the specified point in the mask.
     * Don't forget to call optimizeBounds() after consecutive remove point operation
     * to refresh the mask bounds.
     */
    public void setPoint(int x, int y, int z, boolean value)
    {
        final ROI2DArea slice = getSlice(z, value);

        if (slice != null)
            slice.setPoint(x, y, value);
    }

    /**
     * Add brush point at specified position and for specified Z slice.
     */
    public void addBrush(Point2D pos, int z)
    {
        getSlice(z, true).addBrush(pos);
    }

    /**
     * Remove brush point from the mask at specified position and for specified Z slice.<br>
     * Don't forget to call optimizeBounds() after consecutive remove operation
     * to refresh the mask bounds.
     */
    public void removeBrush(Point2D pos, int z)
    {
        final ROI2DArea slice = getSlice(z, false);

        if (slice != null)
            slice.removeBrush(pos);
    }

    /**
     * Add the specified {@link BooleanMask3D} content to this ROI3DArea
     */
    public void add(BooleanMask3D mask)
    {
        beginUpdate();
        try
        {
            for (Entry<Integer, BooleanMask2D> entry : mask.mask.entrySet())
                add(entry.getKey().intValue(), entry.getValue());
        }
        finally
        {
            endUpdate();
        }
    }

    /**
     * Add the specified BooleanMask2D with the existing slice at given Z position.<br>
     * If there is no slice at this Z position then the method is equivalent to {@link #setSlice(int, ROI2DArea)} with
     * <code>new ROI2DArea(maskSlice)</code>
     * 
     * @param z
     *        the position where the slice must be added
     * @param maskSlice
     *        the 2D boolean mask to merge
     */
    public void add(int z, BooleanMask2D maskSlice)
    {
        if (maskSlice == null)
            return;

        final ROI2DArea currentSlice = getSlice(z);

        if (currentSlice != null)
            // merge slices
            currentSlice.add(maskSlice);
        else
            // add new slice
            setSlice(z, new ROI2DArea(maskSlice));
    }

    /**
     * Exclusively add the specified {@link BooleanMask3D} content to this ROI3DArea
     */
    public void exclusiveAdd(BooleanMask3D mask)
    {
        beginUpdate();
        try
        {
            for (Entry<Integer, BooleanMask2D> entry : mask.mask.entrySet())
                exclusiveAdd(entry.getKey().intValue(), entry.getValue());
        }
        finally
        {
            endUpdate();
        }
    }

    /**
     * Exclusively add the specified BooleanMask2D with the existing slice at given Z position.<br>
     * If there is no slice at this Z position then the method is equivalent to {@link #setSlice(int, ROI2DArea)} with
     * <code>new ROI2DArea(maskSlice)</code>
     * 
     * @param z
     *        the position where the slice must be exclusively added
     * @param maskSlice
     *        the 2D boolean mask to merge
     */
    public void exclusiveAdd(int z, BooleanMask2D maskSlice)
    {
        if (maskSlice == null)
            return;

        final ROI2DArea currentSlice = getSlice(z);

        // merge both slice
        if (currentSlice != null)
        {
            // process exclusive add
            currentSlice.exclusiveAdd(maskSlice);
            // remove it if empty
            if (currentSlice.isEmpty())
                removeSlice(z);
        }
        // add new slice
        else
            setSlice(z, new ROI2DArea(maskSlice));
    }

    /**
     * Intersect the specified {@link BooleanMask3D} content with this ROI3DArea
     */
    public void intersect(BooleanMask3D mask)
    {
        beginUpdate();
        try
        {
            final Set<Integer> keys = mask.mask.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, BooleanMask2D> entry : mask.mask.entrySet())
                intersect(entry.getKey().intValue(), entry.getValue());
        }
        finally
        {
            endUpdate();
        }
    }

    /**
     * Intersect the specified BooleanMask2D with the existing slice at given Z position.
     * 
     * @param z
     *        the position where the slice must be set
     * @param maskSlice
     *        the 2D boolean mask to merge
     */
    public void intersect(int z, BooleanMask2D maskSlice)
    {
        // better to throw an exception here than removing slice
        if (maskSlice == null)
            throw new IllegalArgumentException("Cannot intersect an empty slice in a 3D ROI");

        final ROI2DArea currentSlice = getSlice(z);

        if (currentSlice != null)
        {
            // build ROI from mask
            final ROI2DArea roi = new ROI2DArea(maskSlice);

            // set same position as slice
            roi.setT(currentSlice.getT());
            roi.setZ(currentSlice.getZ());
            roi.setC(currentSlice.getC());
            // compute intersection
            currentSlice.intersect(roi, false);

            // remove it if empty
            if (currentSlice.isEmpty())
                removeSlice(z);
        }
    }

    /**
     * Subtract the specified {@link BooleanMask3D} from this ROI3DArea
     */
    public void subtract(BooleanMask3D mask)
    {
        beginUpdate();
        try
        {
            for (Entry<Integer, BooleanMask2D> entry : mask.mask.entrySet())
                subtract(entry.getKey().intValue(), entry.getValue());
        }
        finally
        {
            endUpdate();
        }
    }

    /**
     * Subtract the specified BooleanMask2D from the existing slice at given Z position.<br>
     * 
     * @param z
     *        the position where the slice must be subtracted
     * @param maskSlice
     *        the 2D boolean mask to subtract
     */
    public void subtract(int z, BooleanMask2D maskSlice)
    {
        if (maskSlice == null)
            return;

        final ROI2DArea currentSlice = getSlice(z);

        // merge both slice
        if (currentSlice != null)
        {
            // process exclusive add
            currentSlice.subtract(maskSlice);
            // remove it if empty
            if (currentSlice.isEmpty())
                removeSlice(z);
        }
    }

    @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 (roi3d instanceof ROI3DArea)
                    add((ROI3DArea) roi3d);
                else
                    add(roi3d.getBooleanMask(true));

                return this;
            }
        }

        return super.add(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 (roi3d instanceof ROI3DArea)
                    exclusiveAdd((ROI3DArea) roi3d);
                else
                    exclusiveAdd(roi3d.getBooleanMask(true));

                return this;
            }
        }

        return super.exclusiveAdd(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 (roi3d instanceof ROI3DArea)
                    intersect((ROI3DArea) roi3d);
                else
                    intersect(roi3d.getBooleanMask(true));

                return this;
            }
        }

        return super.intersect(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 (roi3d instanceof ROI3DArea)
                    subtract((ROI3DArea) roi3d);
                else
                    subtract(roi3d.getBooleanMask(true));

                return this;
            }
        }

        return super.subtract(roi, allowCreate);
    }

    /**
     * Sets the BooleanMask2D slice at given Z position to this 3D ROI
     * 
     * @param z
     *        the position where the slice must be set
     * @param maskSlice
     *        the BooleanMask2D to set
     */
    public void setSlice(int z, BooleanMask2D maskSlice)
    {
        // empty mask --> just remove previous
        if (maskSlice == null)
        {
            removeSlice(z);
            return;
        }

        setSlice(z, new ROI2DArea(maskSlice));
    }

    /**
     * @deprecated Use one of these methods instead :<br>
     *             {@link #setSlice(int, ROI2DArea)}, {@link #setSlice(int, BooleanMask2D)},
     *             {@link #add(int, ROI2DArea)} or {@link #add(BooleanMask3D)}
     */
    @Deprecated
    public void setSlice(int z, ROI2D roiSlice, boolean merge)
    {
        if (roiSlice == null)
            throw new IllegalArgumentException("Cannot add an empty slice in a 3D ROI");

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

        // merge both slice
        if ((currentSlice != null) && merge)
        {
            // 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.getUnion(roiSlice);
        }
        else
            newSlice = roiSlice;

        if (newSlice instanceof ROI2DArea)
            setSlice(z, (ROI2DArea) newSlice);
        else if (newSlice instanceof ROI2D)
            setSlice(z, new ROI2DArea(((ROI2D) newSlice).getBooleanMask(true)));
        else
            throw new IllegalArgumentException(
                    "Can't add the result of the merge operation on 2D slice " + z + ": " + newSlice.getClassName());
    }

    // /**
    // * Merge the specified ROI with the existing slice at given Z position.<br>
    // * If there is no slice at this Z position then the method is equivalent to {@link #setSlice(int, ROI2DArea)}
    // *
    // * @param z
    // * the position where the slice must be set
    // * @param roiSlice
    // * the 2D ROI to merge
    // */
    // public void addROI2DSlice(int z, ROI2D roiSlice)
    // {
    // if (roiSlice == null)
    // return;
    //
    // final ROI2DArea 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 union
    // currentSlice.add(roiSlice, true);
    // }
    // else
    // setSlice(z, new ROI2DArea(roiSlice.getBooleanMask(true)));
    // }

    /**
     * Returns true if the ROI is empty (the mask does not contains any point).
     */
    @Override
    public boolean isEmpty()
    {
        for (ROI2DArea area : slices.values())
            if (!area.isEmpty())
                return false;

        return true;
    }

    /**
     * @deprecated Use {@link #getBooleanMask(boolean)} and {@link BooleanMask3D#getContourPoints()} instead.
     */
    @Deprecated
    public Point3D[] getEdgePoints()
    {
        return getBooleanMask(true).getContourPoints();
    }

    /**
     * @deprecated Use {@link #getBooleanMask(boolean)} and {@link BooleanMask3D#getPoints()} instead.
     */
    @Deprecated
    public Point3D[] getPoints()
    {
        return getBooleanMask(true).getPoints();
    }

    /**
     * @deprecated Use {@link #translate(double, double, double)} instead.
     */
    @Deprecated
    public void translate(double dx, double dy)
    {
        beginUpdate();
        try
        {
            for (ROI2DArea slice : slices.values())
                slice.translate(dx, dy);
        }
        finally
        {
            endUpdate();
        }
    }

    @Override
    public boolean isOverEdge(IcyCanvas canvas, double x, double y, double z)
    {
        final ROI2DArea slice = getSlice((int) z);

        if (slice != null)
            return slice.isOverEdge(canvas, x, y);

        return false;
    }

    /**
     * Set all 2D slices ROI to same position.<br>
     */
    public void setPosition2D(Point2D newPosition)
    {
        beginUpdate();
        try
        {
            for (ROI2DArea slice : slices.values())
                slice.setPosition2D(newPosition);
        }
        finally
        {
            endUpdate();
        }
    }

    /**
     * Set the mask from a BooleanMask3D object.<br>
     * If specified mask is <i>null</i> then ROI is cleared.
     */
    public void setAsBooleanMask(BooleanMask3D mask)
    {
        // mask empty ? --> just clear the ROI
        if ((mask == null) || mask.isEmpty())
            clear();
        else
        {
            final Rectangle3D.Integer bounds3d = mask.bounds;
            final int startZ = bounds3d.z;
            final int sizeZ = bounds3d.sizeZ;
            final BooleanMask2D masks2d[] = new BooleanMask2D[sizeZ];

            for (int z = 0; z < sizeZ; z++)
                masks2d[z] = mask.getMask2D(startZ + z);

            setAsBooleanMask(bounds3d, masks2d);
        }
    }

    /**
     * Set the 3D mask from a 2D boolean mask array
     * 
     * @param rect
     *        the 3D region defined by 2D boolean mask array
     * @param mask
     *        the 3D mask data (array length should be equals to rect.sizeZ)
     */
    public void setAsBooleanMask(Rectangle3D.Integer rect, BooleanMask2D[] mask)
    {
        if (rect.isInfiniteZ())
            throw new IllegalArgumentException("Cannot set infinite Z dimension on the 3D Area ROI.");

        beginUpdate();
        try
        {
            clear();

            for (int z = 0; z < rect.sizeZ; z++)
                setSlice(z + rect.z, new ROI2DArea(mask[z]));
        }
        finally
        {
            endUpdate();
        }
    }

    /**
     * Optimize the bounds size to the minimum surface which still include all mask.<br>
     * You should call it after consecutive remove operations if you directly addressed mask data.
     */
    public void optimizeBounds()
    {
        final Rectangle3D.Integer bounds = getBounds();

        beginUpdate();
        try
        {
            for (int z = bounds.z; z < bounds.z + bounds.sizeZ; z++)
            {
                final ROI2DArea roi = getSlice(z);

                if (roi != null)
                {
                    if (roi.isEmpty())
                        removeSlice(z);
                    else
                    {
                        if (roi.optimizeBounds())
                            roi.roiChanged(true);
                    }
                }
            }
        }
        finally
        {
            endUpdate();
        }
    }

    /**
     * roi changed
     */
    @Override
    public void onChanged(CollapsibleEvent object)
    {
        final ROIEvent event = (ROIEvent) object;

        // do here global process on ROI change
        switch (event.getType())
        {
            case ROI_CHANGED:
                // the painter need to be rebuild
                ((ROI3DAreaPainter) painter).needRebuild = true;
                break;

            case FOCUS_CHANGED:
            case SELECTION_CHANGED:
                ((ROI3DAreaPainter) getOverlay()).updateVtkDisplayProperties();
                break;

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

                if (StringUtil.equals(property, PROPERTY_STROKE) || StringUtil.equals(property, PROPERTY_COLOR)
                        || StringUtil.equals(property, PROPERTY_OPACITY))
                    ((ROI3DAreaPainter) getOverlay()).updateVtkDisplayProperties();
                break;

            default:
                break;
        }

        super.onChanged(object);
    }
}