/**
 * 
 */
package plugins.kernel.roi.roi3d;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.w3c.dom.Node;

import icy.canvas.IcyCanvas;
import icy.canvas.IcyCanvas2D;
import icy.common.CollapsibleEvent;
import icy.math.Line3DIterator;
import icy.painter.Anchor3D;
import icy.painter.Anchor3D.Anchor3DPositionListener;
import icy.painter.OverlayEvent;
import icy.painter.OverlayEvent.OverlayEventType;
import icy.painter.OverlayListener;
import icy.painter.VtkPainter;
import icy.roi.ROI;
import icy.roi.ROI3D;
import icy.roi.ROIEvent;
import icy.roi.edit.Point3DAddedROIEdit;
import icy.roi.edit.Point3DMovedROIEdit;
import icy.roi.edit.Point3DRemovedROIEdit;
import icy.sequence.Sequence;
import icy.system.thread.ThreadUtil;
import icy.type.geom.Line3D;
import icy.type.geom.Shape3D;
import icy.type.point.Point3D;
import icy.type.point.Point5D;
import icy.type.rectangle.Rectangle3D;
import icy.util.EventUtil;
import icy.util.GraphicsUtil;
import icy.util.ShapeUtil;
import icy.util.StringUtil;
import icy.vtk.IcyVtkPanel;
import icy.vtk.VtkUtil;
import plugins.kernel.canvas.VtkCanvas;
import vtk.vtkActor;
import vtk.vtkCellArray;
import vtk.vtkInformation;
import vtk.vtkPoints;
import vtk.vtkPolyData;
import vtk.vtkPolyDataMapper;
import vtk.vtkProp;
import vtk.vtkProperty;
import vtk.vtkRenderer;

/**
 * Base class for 3D shape ROI (working from 3D control points).
 * 
 * @author Stephane Dallongeville
 */
public class ROI3DShape extends ROI3D implements Shape3D
{
    public class ROI3DShapePainter extends ROI3DPainter implements VtkPainter, Runnable
    {
        // VTK 3D objects
        protected vtkPolyData outline;
        protected vtkPolyDataMapper outlineMapper;
        protected vtkActor outlineActor;
        protected vtkInformation vtkInfo;
        protected vtkCellArray vCells;
        protected vtkPoints vPoints;
        protected vtkPolyData polyData;
        protected vtkPolyDataMapper polyMapper;
        protected vtkActor actor;
        // 3D internal
        protected boolean needRebuild;
        protected double scaling[];
        protected WeakReference<VtkCanvas> canvas3d;
        protected Set<Anchor3D> actorsToAdd;
        protected Set<Anchor3D> actorsToRemove;

        public ROI3DShapePainter()
        {
            super();

            // don't create VTK object on constructor
            outline = null;
            outlineMapper = null;
            outlineActor = null;
            vtkInfo = null;
            vCells = null;
            vPoints = null;
            polyData = null;
            polyMapper = null;
            actor = null;

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

            actorsToAdd = new HashSet<Anchor3D>();
            actorsToRemove = new HashSet<Anchor3D>();

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

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

            // release allocated VTK resources
            if (actor != null)
                actor.Delete();
            if (polyMapper != null)
                polyMapper.Delete();
            if (polyData != null)
                polyData.Delete();
            if (vPoints != null)
                vPoints.Delete();
            if (vCells != null)
                vCells.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);

            // init poly data object
            polyData = new vtkPolyData();
            polyMapper = new vtkPolyDataMapper();
            polyMapper.SetInputData(polyData);
            actor = new vtkActor();
            actor.SetMapper(polyMapper);

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

            outlineActor.GetProperty().SetColor(r, g, b);
            final vtkProperty property = actor.GetProperty();
            property.SetPointSize(getStroke());
            property.SetColor(r, g, b);
        }

        /**
         * update 3D painter for 3D canvas (called only when VTK is loaded).
         */
        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 scaling
            final double xs = scaling[0];
            final double ys = scaling[1];
            final double zs = scaling[2];

            // update polydata
            final int numPts = controlPoints.size();
            final double[][] vertices = new double[numPts][3];
            final int[] indexes = new int[numPts + 1];
            indexes[0] = numPts;

            if (!controlPoints.isEmpty())
            {
                // add all controls point position
                for (int i = 0; i < numPts; i++)
                {
                    final Point3D point = controlPoints.get(i).getPosition();
                    final double[] vertex = vertices[i];

                    vertex[0] = point.getX() * xs;
                    vertex[1] = point.getY() * ys;
                    vertex[2] = point.getZ() * zs;

                    indexes[i + 1] = i;
                }
            }

            final vtkCellArray previousCells = vCells;
            final vtkPoints previousPoints = vPoints;
            vCells = VtkUtil.getCells(1, indexes);
            vPoints = VtkUtil.getPoints(vertices);

            // get bounds
            final Rectangle3D bounds = getBounds3D();

            // 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() * xs, bounds.getMaxX() * xs, bounds.getMinY() * ys,
                        bounds.getMaxY() * ys, bounds.getMinZ() * zs, bounds.getMaxZ() * zs, canvas);
                outlineMapper.Update();
                // update polygon data from cell and points
                polyData.SetPoints(vPoints);
                polyData.SetLines(vCells);
                polyMapper.Update();

                // release previous allocated VTK objects
                if (previousCells != null)
                    previousCells.Delete();
                if (previousPoints != null)
                    previousPoints.Delete();
            }
            finally
            {
                vtkPanel.unlock();
            }

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

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

            final VtkCanvas cnv = canvas3d.get();
            final vtkProperty vtkProperty = actor.GetProperty();
            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);
                }
                vtkProperty.SetColor(r, g, b);
                vtkProperty.SetPointSize(strk);
                vtkProperty.SetLineWidth(strk);
                // opacity here is about ROI content, global opacity is handled by Layer
                // vtkProperty.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
        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 = (actor != null) && (actor == ((VtkCanvas) canvas).getPickedObject());

                setFocused(focused);

                return focused;
            }

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

        @Override
        public void keyPressed(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            if (isSelected() && !isReadOnly())
            {
                if (isActiveFor(canvas))
                {
                    ROI3DShape.this.beginUpdate();
                    try
                    {
                        // get control points list
                        final List<Anchor3D> controlPoints = getControlPoints();

                        // send event to controls points first
                        for (Anchor3D pt : controlPoints)
                            pt.keyPressed(e, imagePoint, canvas);

                        // specific action for ROI3DPolyLine
                        if (!e.isConsumed())
                        {
                            final Sequence sequence = canvas.getSequence();

                            switch (e.getKeyCode())
                            {
                                case KeyEvent.VK_DELETE:
                                case KeyEvent.VK_BACK_SPACE:
                                    final Anchor3D selectedPoint = getSelectedPoint();

                                    // try to remove selected point
                                    if (removeSelectedPoint(canvas))
                                    {
                                        // consume event
                                        e.consume();

                                        // add undo operation
                                        if (sequence != null)
                                            sequence.addUndoableEdit(new Point3DRemovedROIEdit(ROI3DShape.this,
                                                    controlPoints, selectedPoint));
                                    }
                                    break;
                            }
                        }
                    }
                    finally
                    {
                        ROI3DShape.this.endUpdate();
                    }
                }
            }

            // then send event to parent
            super.keyPressed(e, imagePoint, canvas);
        }

        @Override
        public void keyReleased(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            if (isSelected() && !isReadOnly())
            {
                if (isActiveFor(canvas))
                {
                    // check we can do the action
                    if (imagePoint != null)
                    {
                        ROI3DShape.this.beginUpdate();
                        try
                        {
                            // send event to controls points first
                            synchronized (controlPoints)
                            {
                                for (Anchor3D pt : controlPoints)
                                    pt.keyReleased(e, imagePoint, canvas);
                            }
                        }
                        finally
                        {
                            ROI3DShape.this.endUpdate();
                        }
                    }
                }
            }

            // then send event to parent
            super.keyReleased(e, imagePoint, canvas);
        }

        @Override
        public void mousePressed(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            if (isActiveFor(canvas))
            {
                // check we can do the action
                if (isSelected() && !isReadOnly())
                {
                    ROI3DShape.this.beginUpdate();
                    try
                    {
                        // send event to controls points first
                        synchronized (controlPoints)
                        {
                            for (Anchor3D pt : controlPoints)
                                pt.mousePressed(e, imagePoint, canvas);
                        }

                        // specific action for this ROI
                        if (!e.isConsumed())
                        {
                            if (imagePoint != null)
                            {
                                // left button action
                                if (EventUtil.isLeftMouseButton(e))
                                {
                                    // ROI should not be focused to add point (for multi selection)
                                    if (!isFocused())
                                    {
                                        final boolean insertMode = EventUtil.isControlDown(e);

                                        // insertion mode or creating the ROI ? --> add a new point
                                        if (insertMode || isCreating())
                                        {
                                            // try to add point
                                            final Anchor3D point = addNewPoint(imagePoint.toPoint3D(), insertMode);

                                            // point added ?
                                            if (point != null)
                                            {
                                                // consume event
                                                e.consume();

                                                final Sequence sequence = canvas.getSequence();

                                                // add undo operation
                                                if (sequence != null)
                                                    sequence.addUndoableEdit(
                                                            new Point3DAddedROIEdit(ROI3DShape.this, point));
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                    finally
                    {
                        ROI3DShape.this.endUpdate();
                    }
                }
            }

            // then send event to parent
            super.mousePressed(e, imagePoint, canvas);
        }

        @Override
        public void mouseReleased(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // not anymore the first move
            firstMove = false;

            if (isSelected() && !isReadOnly())
            {
                // send event to controls points first
                if (isActiveFor(canvas))
                {
                    final Sequence sequence = canvas.getSequence();

                    ROI3DShape.this.beginUpdate();
                    try
                    {
                        // default anchor action on mouse release
                        synchronized (controlPoints)
                        {
                            for (Anchor3D pt : controlPoints)
                                pt.mouseReleased(e, imagePoint, canvas);
                        }
                    }
                    finally
                    {
                        ROI3DShape.this.endUpdate();
                    }

                    // prevent undo operation merging
                    if (sequence != null)
                        sequence.getUndoManager().noMergeForNextEdit();
                }
            }

            // then send event to parent
            super.mouseReleased(e, imagePoint, canvas);
        }

        @Override
        public void mouseClick(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            if (isSelected() && !isReadOnly())
            {
                // send event to controls points first
                if (isActiveFor(canvas))
                {
                    ROI3DShape.this.beginUpdate();
                    try
                    {
                        // default anchor action on mouse click
                        synchronized (controlPoints)
                        {
                            for (Anchor3D pt : controlPoints)
                                pt.mouseClick(e, imagePoint, canvas);
                        }
                    }
                    finally
                    {
                        ROI3DShape.this.endUpdate();
                    }
                }
            }

            // then send event to parent
            super.mouseClick(e, imagePoint, canvas);
        }

        @Override
        public void mouseDrag(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            if (isActiveFor(canvas))
            {
                // check we can do the action
                if (isSelected() && !isReadOnly())
                {
                    final Sequence sequence = canvas.getSequence();

                    // send event to controls points first
                    ROI3DShape.this.beginUpdate();
                    try
                    {
                        // default anchor action on mouse drag
                        synchronized (controlPoints)
                        {
                            for (Anchor3D pt : controlPoints)
                            {
                                final Point3D savedPosition;

                                // don't want to undo position change on first creation movement
                                if ((sequence != null) && (!isCreating() || !firstMove))
                                    savedPosition = pt.getPosition();
                                else
                                    savedPosition = null;

                                pt.mouseDrag(e, imagePoint, canvas);

                                // position changed and undo supported --> add undo operation
                                if ((sequence != null) && (savedPosition != null)
                                        && !savedPosition.equals(pt.getPosition()))
                                    sequence.addUndoableEdit(
                                            new Point3DMovedROIEdit(ROI3DShape.this, pt, savedPosition));
                            }
                        }
                    }
                    finally
                    {
                        ROI3DShape.this.endUpdate();
                    }
                }
            }

            // then send event to parent
            super.mouseDrag(e, imagePoint, canvas);
        }

        @Override
        public void mouseMove(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            if (isActiveFor(canvas))
            {
                // check we can do the action
                if (isSelected() && !isReadOnly())
                {
                    // send event to controls points first
                    ROI3DShape.this.beginUpdate();
                    try
                    {
                        // refresh control point state
                        synchronized (controlPoints)
                        {
                            for (Anchor3D pt : controlPoints)
                                pt.mouseMove(e, imagePoint, canvas);
                        }
                    }
                    finally
                    {
                        ROI3DShape.this.endUpdate();
                    }
                }
            }

            // then send event to parent
            super.mouseMove(e, imagePoint, canvas);
        }

        /**
         * Draw the ROI
         */
        @Override
        public void drawROI(Graphics2D g, Sequence sequence, IcyCanvas canvas)
        {
            if (canvas instanceof IcyCanvas2D)
            {
                // not supported
                if (g == null)
                    return;

                final Rectangle2D bounds = getBounds3D().toRectangle2D();

                // enlarge bounds with stroke
                final double over = getAdjustedStroke(canvas) * 2;
                ShapeUtil.enlarge(bounds, over, over, true);

                // define LOD level
                final boolean shapeVisible = isVisible(bounds, g, canvas);

                if (shapeVisible)
                {
                    final boolean small = isSmall(bounds, g, canvas);

                    // draw shape
                    drawShape(g, sequence, canvas, small);

                    // draw control points (only if not tiny)
                    if (!isTiny(bounds, g, canvas) && isSelected() && !isReadOnly())
                    {
                        // draw control point if selected
                        synchronized (controlPoints)
                        {
                            for (Anchor3D pt : controlPoints)
                                pt.paint(g, sequence, canvas, small);
                        }
                    }
                }
            }

            if (canvas instanceof VtkCanvas)
            {
                // 3D canvas
                final VtkCanvas cnv = (VtkCanvas) canvas;
                // update reference if needed
                if (canvas3d.get() != cnv)
                    canvas3d = new WeakReference<VtkCanvas>(cnv);

                // initialize VTK objects if not yet done
                if (actor == null)
                    initVtkObjects();

                // 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)
                {
                    // request rebuild 3D objects
                    ThreadUtil.runSingle(this);
                    needRebuild = false;
                }

                final vtkRenderer renderer = cnv.getRenderer();

                // need to remove control points actor ?
                synchronized (actorsToRemove)
                {
                    for (Anchor3D anchor : actorsToRemove)
                        for (vtkProp prop : anchor.getProps())
                            VtkUtil.removeProp(renderer, prop);

                    // done
                    actorsToRemove.clear();
                }
                // need to add control points actor ?
                synchronized (actorsToAdd)
                {
                    for (Anchor3D anchor : actorsToAdd)
                        for (vtkProp prop : anchor.getProps())
                            VtkUtil.addProp(renderer, prop);

                    // done
                    actorsToAdd.clear();
                }

                // needed to forward paint event to control point
                synchronized (controlPoints)
                {
                    for (Anchor3D pt : controlPoints)
                        pt.paint(null, sequence, canvas);
                }
            }
        }

        /**
         * Draw the shape in specified Graphics2D context.<br>
         * Override {@link #drawShape(Graphics2D, Sequence, IcyCanvas, boolean, boolean)} instead if possible.
         */
        protected void drawShape(Graphics2D g, Sequence sequence, IcyCanvas canvas, boolean simplified)
        {
            drawShape(g, sequence, canvas, simplified, true);
        }

        /**
         * Draw the shape in specified Graphics2D context.<br>
         * Default implementation just draw '3D' lines between all controls points
         */
        protected void drawShape(Graphics2D g, Sequence sequence, IcyCanvas canvas, boolean simplified,
                boolean connectLastPoint)
        {
            final List<Point3D> points = getPointsInternal();
            final Graphics2D g2 = (Graphics2D) g.create();

            // normal rendering without selection --> draw border first
            if (!simplified && !isSelected())
            {
                // draw border
                g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke + 1d), BasicStroke.CAP_BUTT,
                        BasicStroke.JOIN_MITER));
                g2.setColor(Color.black);

                for (int i = 1; i < points.size(); i++)
                    drawLine3D(g2, sequence, canvas, points.get(i - 1), points.get(i));
                // connect last point
                if (connectLastPoint && (points.size() > 2))
                    drawLine3D(g2, sequence, canvas, points.get(points.size() - 1), points.get(0));
            }

            // then draw shape
            g2.setStroke(new BasicStroke(
                    (float) ROI.getAdjustedStroke(canvas, (!simplified && isSelected()) ? stroke + 1 : stroke),
                    BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER));
            g2.setColor(getDisplayColor());

            for (int i = 1; i < points.size(); i++)
                drawLine3D(g2, sequence, canvas, points.get(i - 1), points.get(i));
            // connect last point
            if (connectLastPoint && (points.size() > 2))
                drawLine3D(g2, sequence, canvas, points.get(points.size() - 1), points.get(0));

            g2.dispose();
        }

        /**
         * Returns <code>true</code> if the specified bounds should be considered as "tiny" in the
         * specified canvas / graphics context.
         */
        protected boolean isVisible(Rectangle2D bounds, Graphics2D g, IcyCanvas canvas)
        {
            return GraphicsUtil.isVisible(g, bounds);
        }

        /**
         * Returns <code>true</code> if the specified bounds should be considered as "tiny" in the
         * specified canvas / graphics context.
         */
        protected boolean isSmall(Rectangle2D bounds, Graphics2D g, IcyCanvas canvas)
        {
            if (isCreating())
                return false;

            final double scale = Math.max(Math.abs(canvas.getScaleX()), Math.abs(canvas.getScaleY()));
            final double size = Math.max(scale * bounds.getWidth(), scale * bounds.getHeight());

            return size < LOD_SMALL;
        }

        /**
         * Returns <code>true</code> if the specified bounds should be considered as "tiny" in the
         * specified canvas / graphics context.
         */
        protected boolean isTiny(Rectangle2D bounds, Graphics2D g, IcyCanvas canvas)
        {
            if (isCreating())
                return false;

            final double scale = Math.max(Math.abs(canvas.getScaleX()), Math.abs(canvas.getScaleY()));
            final double size = Math.max(scale * bounds.getWidth(), scale * bounds.getHeight());

            return size < LOD_TINY;
        }

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

                // also change colors of controls points
                final Color focusedColor = getFocusedColor();

                synchronized (controlPoints)
                {
                    for (Anchor3D anchor : controlPoints)
                    {
                        anchor.setColor(value);
                        anchor.setSelectedColor(focusedColor);
                    }
                }
            }
            finally
            {
                endUpdate();
            }
        }

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

            final List<vtkProp> result = new ArrayList<vtkProp>();

            // add VTK objects from ROI shape
            result.add(actor);
            result.add(outlineActor);

            // then add VTK objects from controls points
            synchronized (controlPoints)
            {
                for (Anchor3D pt : controlPoints)
                    for (vtkProp prop : pt.getProps())
                        result.add(prop);
            }

            return result.toArray(new vtkProp[result.size()]);
        }

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

    /**
     * Draw a 3D line in specified graphics object
     */
    protected static void drawLine3D(Graphics2D g, Sequence sequence, IcyCanvas canvas, Point3D p1, Point3D p2)
    {
        final Line2D line2d = new Line2D.Double();

        // get canvas Z position
        final int cnvZ = canvas.getPositionZ();
        // calculate z fade range
        final double zRange = Math.min(10d, Math.max(3d, sequence.getSizeZ() / 8d));

        // same Z, don't need to split lines
        if (p1.getZ() == p2.getZ())
            drawSegment3D(g, p1, p2, zRange, cnvZ, line2d);
        else
        {
            final Line3DIterator it = new Line3DIterator(new Line3D(p1, p2), 4d / canvas.getScaleX());
            // start position
            Point3D pos = it.next();

            do
            {
                // get next position
                final Point3D nextPos = it.next();
                // draw line
                drawSegment3D(g, pos, nextPos, zRange, cnvZ, line2d);
                // update current pos
                pos = nextPos;
            }
            while (it.hasNext());
        }
    }

    /**
     * Draw a 3D line in specified graphics object
     */
    protected static void drawSegment3D(Graphics2D g, Point3D p1, Point3D p2, double zRange, int canvasZ, Line2D line2d)
    {
        // get Line Z pos
        final double meanZ = (p1.getZ() + p2.getZ()) / 2d;
        // get delta Z (difference between canvas Z position and line Z pos)
        final double dz = Math.abs(meanZ - canvasZ);

        // not visible on this Z position
        if (dz > zRange)
            return;

        // ratio for size / opacity
        final float ratio = 1f - (float) (dz / zRange);
        final Composite prevComposite = g.getComposite();

        if (ratio != 1f)
            GraphicsUtil.mixAlpha(g, ratio);

        // draw line
        line2d.setLine(p1.getX(), p1.getY(), p2.getX(), p2.getY());
        g.draw(line2d);

        // restore composite
        g.setComposite(prevComposite);
    }

    public static final String ID_POINTS = "points";
    public static final String ID_POINT = "point";

    /**
     * Polyline3D shape (in image coordinates)
     */
    protected final Shape3D shape;
    /**
     * control points
     */
    protected final List<Anchor3D> controlPoints;

    /**
     * internals
     */
    protected final Anchor3DPositionListener anchor2DPositionListener;
    protected final OverlayListener anchor2DOverlayListener;
    protected boolean firstMove;

    /**
     * 
     */
    public ROI3DShape(Shape3D shape)
    {
        super();

        this.shape = shape;
        controlPoints = new ArrayList<Anchor3D>();
        firstMove = true;

        anchor2DPositionListener = new Anchor3DPositionListener()
        {
            @Override
            public void positionChanged(Anchor3D source)
            {
                controlPointPositionChanged(source);
            }
        };
        anchor2DOverlayListener = new OverlayListener()
        {
            @Override
            public void overlayChanged(OverlayEvent event)
            {
                controlPointOverlayChanged(event);
            }
        };
    }

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

    @Override
    protected ROI3DShapePainter createPainter()
    {
        return new ROI3DShapePainter();
    }

    /**
     * build a new anchor with specified position
     */
    protected Anchor3D createAnchor(Point3D pos)
    {
        return new Anchor3D(pos.getX(), pos.getY(), pos.getZ(), getColor(), getFocusedColor());
    }

    /**
     * @return the shape
     */
    public Shape3D getShape()
    {
        return shape;
    }

    /**
     * Return true if this ROI support adding new point
     */
    public boolean canAddPoint()
    {
        return true;
    }

    /**
     * Return true if this ROI support removing point
     */
    public boolean canRemovePoint()
    {
        return true;
    }

    /**
     * Internal use only
     */
    protected void addPoint(Anchor3D pt)
    {
        addPoint(pt, -1);
    }

    /**
     * Internal use only, use {@link #addNewPoint(Point3D, boolean)} instead.
     */
    public void addPoint(Anchor3D pt, int index)
    {
        // set visible state
        pt.setVisible(isSelected());

        pt.addPositionListener(anchor2DPositionListener);
        pt.addOverlayListener(anchor2DOverlayListener);

        if (index == -1)
            controlPoints.add(pt);
        else
            controlPoints.add(index, pt);

        synchronized (((ROI3DShapePainter) getOverlay()).actorsToAdd)
        {
            // store it in the "actor to add" list
            ((ROI3DShapePainter) getOverlay()).actorsToAdd.add(pt);
        }
        synchronized (((ROI3DShapePainter) getOverlay()).actorsToRemove)
        {
            // and remove it from the "actor to remove" list
            ((ROI3DShapePainter) getOverlay()).actorsToRemove.remove(pt);
        }

        roiChanged(true);
    }

    /**
     * Add a new point to the Polyline 3D ROI.
     * 
     * @param pos
     *        position of the new point
     * @param insert
     *        if set to <code>true</code> the new point will be inserted between the 2 closest
     *        points (in pixels distance) else the new point is inserted at the end of the point
     *        list
     * @return the new created Anchor3D point
     */
    public Anchor3D addNewPoint(Point3D pos, boolean insert)
    {
        if (!canAddPoint())
            return null;

        final Anchor3D pt = createAnchor(pos);

        if (insert)
            // insert mode ? --> place the new point with closest points
            addPoint(pt, getInsertPointPosition(pos));
        else
            // just add the new point at last position
            addPoint(pt);

        // always select
        pt.setSelected(true);

        return pt;
    }

    /**
     * internal use only
     */
    protected boolean removePoint(IcyCanvas canvas, Anchor3D pt)
    {
        boolean empty;

        pt.removeOverlayListener(anchor2DOverlayListener);
        pt.removePositionListener(anchor2DPositionListener);

        synchronized (controlPoints)
        {
            controlPoints.remove(pt);
            empty = controlPoints.isEmpty();
        }

        synchronized (((ROI3DShapePainter) getOverlay()).actorsToRemove)
        {
            // store it in the "actor to remove" list
            ((ROI3DShapePainter) getOverlay()).actorsToRemove.add(pt);
        }
        synchronized (((ROI3DShapePainter) getOverlay()).actorsToAdd)
        {
            // and remove it from the "actor to add" list
            ((ROI3DShapePainter) getOverlay()).actorsToAdd.remove(pt);
        }

        // empty ROI ? --> remove from all sequence
        if (empty)
            remove();
        else
            roiChanged(true);

        return true;
    }

    /**
     * This method give you lower level access on point remove operation but can be unsafe.<br/>
     * Use {@link #removeSelectedPoint(IcyCanvas)} when possible.
     */
    public boolean removePoint(Anchor3D pt)
    {
        return removePoint(null, pt);
    }

    /**
     * internal use only (used for fast clear)
     */
    protected void removeAllPoint()
    {
        synchronized (controlPoints)
        {
            synchronized (((ROI3DShapePainter) getOverlay()).actorsToRemove)
            {
                // store all points in the "actor to remove" list
                ((ROI3DShapePainter) getOverlay()).actorsToRemove.addAll(controlPoints);
            }
            synchronized (((ROI3DShapePainter) getOverlay()).actorsToAdd)
            {
                // and remove them from the "actor to add" list
                ((ROI3DShapePainter) getOverlay()).actorsToAdd.removeAll(controlPoints);
            }

            for (Anchor3D pt : controlPoints)
            {
                pt.removeOverlayListener(anchor2DOverlayListener);
                pt.removePositionListener(anchor2DPositionListener);
            }

            controlPoints.clear();
        }
    }

    /**
     * Remove the current selected point.
     */
    public boolean removeSelectedPoint(IcyCanvas canvas)
    {
        if (!canRemovePoint())
            return false;

        final Anchor3D selectedPoint = getSelectedPoint();

        if (selectedPoint == null)
            return false;

        synchronized (controlPoints)
        {
            // try to remove point
            if (!removePoint(canvas, selectedPoint))
                return false;

            // still have control points
            if (controlPoints.size() > 0)
            {
                // save the point position
                final Point3D imagePoint = selectedPoint.getPosition();

                // select a new point if possible
                if (controlPoints.size() > 0)
                    selectPointAt(canvas, imagePoint);
            }
        }

        return true;
    }

    protected Anchor3D getSelectedPoint()
    {
        synchronized (controlPoints)
        {
            for (Anchor3D pt : controlPoints)
                if (pt.isSelected())
                    return pt;
        }

        return null;
    }

    @Override
    public boolean hasSelectedPoint()
    {
        return (getSelectedPoint() != null);
    }

    protected boolean selectPointAt(IcyCanvas canvas, Point3D imagePoint)
    {
        synchronized (controlPoints)
        {
            // find the new selected control point
            for (Anchor3D pt : controlPoints)
            {
                // control point is overlapped ?
                if (pt.isOver(canvas, imagePoint))
                {
                    // select it
                    pt.setSelected(true);
                    return true;
                }
            }
        }

        return false;
    }

    @Override
    public void unselectAllPoints()
    {
        beginUpdate();
        try
        {
            synchronized (controlPoints)
            {
                // unselect all point
                for (Anchor3D pt : controlPoints)
                    pt.setSelected(false);
            }
        }
        finally
        {
            endUpdate();
        }
    };

    @SuppressWarnings("static-method")
    protected double getTotalDistance(List<Point3D> points, double factorX, double factorY, double factorZ)
    {
        // default implementation
        return Point3D.getTotalDistance(points, factorX, factorY, factorZ, true);
    }

    @Override
    public double getLength(Sequence sequence)
    {
        return getTotalDistance(getPointsInternal(), sequence.getPixelSizeX(), sequence.getPixelSizeY(),
                sequence.getPixelSizeZ());
    }

    @Override
    public double computeSurfaceArea(Sequence sequence)
    {
        return 0d;
    }

    @Override
    public double computeNumberOfContourPoints()
    {
        return getTotalDistance(getPointsInternal(), 1d, 1d, 1d);
    }

    /**
     * Find best insert position for specified point
     */
    protected int getInsertPointPosition(Point3D pos)
    {
        final List<Point3D> points = getPointsInternal();

        final int size = points.size();
        // by default we use last position
        int result = size;
        double minDistance = Double.MAX_VALUE;

        // we try all cases
        for (int i = size; i >= 0; i--)
        {
            // add point at current position
            points.add(i, pos);

            // calculate total distance
            final double d = getTotalDistance(points, 1d, 1d, 1d);
            // minimum distance ?
            if (d < minDistance)
            {
                // save index
                minDistance = d;
                result = i;
            }

            // remove point from current position
            points.remove(i);
        }

        return result;
    }

    // @Override
    // public boolean isOverPoint(IcyCanvas canvas, double x, double y)
    // {
    // if (isSelected())
    // {
    // for (Anchor3D pt : controlPoints)
    // if (pt.isOver(canvas, x, y))
    // return true;
    // }
    //
    // return false;
    // }

    /**
     * Return the list of control points for this ROI.
     */
    public List<Anchor3D> getControlPoints()
    {
        synchronized (controlPoints)
        {
            return new ArrayList<Anchor3D>(controlPoints);
        }
    }

    /**
     * Return the list of position for all control points of the ROI.
     */
    public List<Point3D> getPoints()
    {
        final List<Point3D> result = new ArrayList<Point3D>();

        synchronized (controlPoints)
        {
            for (Anchor3D pt : controlPoints)
                result.add(pt.getPosition());
        }

        return result;
    }

    /**
     * Return the list of positions of control points for this ROI.<br>
     * This is the direct internal position reference, don't modify them !
     */
    protected List<Point3D> getPointsInternal()
    {
        final List<Point3D> result = new ArrayList<Point3D>();

        synchronized (controlPoints)
        {
            for (Anchor3D pt : controlPoints)
                result.add(pt.getPositionInternal());
        }

        return result;
    }

    /**
     * Returns true if specified point coordinates overlap the ROI edge.
     */
    @Override
    public boolean isOverEdge(IcyCanvas canvas, double x, double y, double z)
    {
        // use bigger stroke for isOver test for easier intersection
        final double strk = painter.getAdjustedStroke(canvas) * 3;
        final Rectangle3D rect = new Rectangle3D.Double(x - (strk * 0.5), y - (strk * 0.5), z - (strk * 0.5), strk,
                strk, strk);

        return intersects(rect);
    }

    @Override
    public boolean contains(Point3D p)
    {
        return shape.contains(p);
    }

    @Override
    public boolean contains(Rectangle3D r)
    {
        return shape.contains(r);
    }

    @Override
    public boolean contains(double x, double y, double z)
    {
        return shape.contains(x, y, z);
    }

    @Override
    public boolean contains(double x, double y, double z, double sizeX, double sizeY, double sizeZ)
    {
        return shape.contains(x, y, z, sizeX, sizeY, sizeZ);
    }

    @Override
    public boolean intersects(Rectangle3D r)
    {
        return shape.intersects(r);
    }

    @Override
    public boolean intersects(double x, double y, double z, double sizeX, double sizeY, double sizeZ)
    {
        return shape.intersects(x, y, z, sizeX, sizeY, sizeZ);
    }

    @Override
    public Rectangle3D computeBounds3D()
    {
        return shape.getBounds();
    }

    @Override
    public boolean canTranslate()
    {
        return true;
    }

    @Override
    public void translate(double dx, double dy, double dz)
    {
        beginUpdate();
        try
        {
            synchronized (controlPoints)
            {
                for (Anchor3D pt : controlPoints)
                    pt.translate(dx, dy, dz);
            }
        }
        finally
        {
            endUpdate();
        }
    }

    /**
     * Called when anchor position changed
     */
    public void controlPointPositionChanged(Anchor3D source)
    {
        // anchor(s) position changed --> ROI changed
        roiChanged(true);
    }

    /**
     * Called when anchor overlay changed
     */
    public void controlPointOverlayChanged(OverlayEvent event)
    {
        // we only mind about painter change from anchor...
        if (event.getType() == OverlayEventType.PAINTER_CHANGED)
        {
            // we have a control point selected --> remove focus on ROI
            if (hasSelectedPoint())
                setFocused(false);

            // anchor changed --> ROI painter changed
            getOverlay().painterChanged();
        }
    }

    /**
     * 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:
                // refresh shape
                updateShape();
                break;

            case FOCUS_CHANGED:
                ((ROI3DShapePainter) getOverlay()).updateVtkDisplayProperties();
                break;

            case SELECTION_CHANGED:
                final boolean s = isSelected();

                beginUpdate();
                try
                {
                    // set control points visible or not
                    synchronized (controlPoints)
                    {
                        for (Anchor3D pt : controlPoints)
                            pt.setVisible(s);
                    }

                    // unselect if not visible
                    if (!s)
                        unselectAllPoints();
                }
                finally
                {
                    endUpdate();
                }

                ((ROI3DShapePainter) 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))
                    ((ROI3DShapePainter) getOverlay()).updateVtkDisplayProperties();
                break;

            default:
                break;
        }

        super.onChanged(object);
    }

    @Override
    public double computeNumberOfPoints()
    {
        return 0d;
    }

    /**
     * Rebuild shape.<br>
     * This method should be overridden by derived classes which<br>
     * have to call the super.updateShape() method at end.
     */
    protected void updateShape()
    {
        // the shape should have been rebuilt here
        ((ROI3DShapePainter) painter).needRebuild = true;
    }

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

            firstMove = false;
            // unselect all control points
            unselectAllPoints();
        }
        finally
        {
            endUpdate();
        }

        return true;
    }
}