/*
 * Copyright 2010-2013 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Icy is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package plugins.kernel.roi.roi2d;

import icy.canvas.Canvas3D;
import icy.canvas.IcyCanvas;
import icy.canvas.IcyCanvas2D;
import icy.common.EventHierarchicalChecker;
import icy.main.Icy;
import icy.painter.Anchor2D;
import icy.painter.Anchor2D.Anchor2DListener;
import icy.painter.OverlayEvent;
import icy.painter.OverlayEvent.OverlayEventType;
import icy.painter.OverlayListener;
import icy.painter.PainterEvent;
import icy.painter.PathAnchor2D;
import icy.painter.VtkPainter;
import icy.roi.ROI;
import icy.roi.ROI2D;
import icy.roi.ROIEvent;
import icy.sequence.Sequence;
import icy.type.point.Point3D;
import icy.type.point.Point5D;
import icy.util.EventUtil;
import icy.util.ShapeUtil;
import icy.util.ShapeUtil.BooleanOperator;
import icy.vtk.VtkUtil;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import vtk.vtkActor;
import vtk.vtkPointSet;
import vtk.vtkPolyData;
import vtk.vtkPolyDataMapper;
import vtk.vtkProp;

/**
 * @author Stephane
 */
public abstract class ROI2DShape extends ROI2D implements Shape, Anchor2DListener, OverlayListener
{
    public class ROI2DShapePainter extends ROI2DPainter implements VtkPainter
    {
        // VTK 3D objects, we use Object to prevent UnsatisfiedLinkError
        final Object polyData;
        final Object polyMapper;
        final Object actor;
        // 3D internal
        boolean needRebuild;
        double scaling[];

        public ROI2DShapePainter()
        {
            super();

            // avoid to use vtk objects when vtk library is missing.
            if (Icy.isVtkLibraryLoaded())
            {
                // init 3D painters stuff
                polyData = new vtkPolyData();

                polyMapper = new vtkPolyDataMapper();
                ((vtkPolyDataMapper) polyMapper).SetInput((vtkPolyData) polyData);

                actor = new vtkActor();
                ((vtkActor) actor).SetMapper((vtkPolyDataMapper) polyMapper);
            }
            else
            {
                polyData = null;
                polyMapper = null;
                actor = null;
            }

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

            needRebuild = true;
        }

        /**
         * update 3D painter for 3D canvas (called only when vtk is loaded).
         */
        protected void rebuild3DPainter(Canvas3D canvas)
        {
            final Sequence seq = canvas.getSequence();

            // nothing to update
            if (seq == null)
                return;

            final ArrayList<Point3D.Double> point3DList = new ArrayList<Point3D.Double>();
            final ArrayList<Poly3D> polyList = new ArrayList<Poly3D>();
            final double[] coords = new double[6];

            final double nbSlice = seq.getSizeZ(canvas.getPositionT());
            // use flat path
            final PathIterator path = getPathIterator(null, 0.5d);

            // starting position
            double xm = 0d;
            double ym = 0d;
            double x0 = 0d;
            double y0 = 0d;
            double x1 = 0d;
            double y1 = 0d;
            int ind;

            // build point data
            while (!path.isDone())
            {
                int segType = path.currentSegment(coords);

                switch (segType)
                {
                    case PathIterator.SEG_MOVETO:
                        x0 = xm = coords[0];
                        y0 = ym = coords[1];
                        break;

                    case PathIterator.SEG_LINETO:
                        x1 = coords[0];
                        y1 = coords[1];

                        ind = point3DList.size();

                        point3DList.add(new Point3D.Double(x0 * scaling[0], y0 * scaling[1], 0));
                        point3DList.add(new Point3D.Double(x1 * scaling[0], y1 * scaling[1], 0));
                        point3DList.add(new Point3D.Double(x0 * scaling[0], y0 * scaling[1], nbSlice * scaling[2]));
                        point3DList.add(new Point3D.Double(x1 * scaling[0], y1 * scaling[1], nbSlice * scaling[2]));
                        polyList.add(new Poly3D(1 + ind, 2 + ind, 0 + ind));
                        polyList.add(new Poly3D(3 + ind, 2 + ind, 1 + ind));

                        x0 = x1;
                        y0 = y1;
                        break;

                    case PathIterator.SEG_CLOSE:
                        x1 = xm;
                        y1 = ym;

                        ind = point3DList.size();

                        point3DList.add(new Point3D.Double(x0 * scaling[0], y0 * scaling[1], 0));
                        point3DList.add(new Point3D.Double(x1 * scaling[0], y1 * scaling[1], 0));
                        point3DList.add(new Point3D.Double(x0 * scaling[0], y0 * scaling[1], nbSlice * scaling[2]));
                        point3DList.add(new Point3D.Double(x1 * scaling[0], y1 * scaling[1], nbSlice * scaling[2]));
                        polyList.add(new Poly3D(1 + ind, 2 + ind, 0 + ind));
                        polyList.add(new Poly3D(3 + ind, 2 + ind, 1 + ind));

                        x0 = x1;
                        y0 = y1;
                        break;
                }

                path.next();
            }

            // convert to array
            final double[][] vertices = new double[point3DList.size()][3];
            final int[][] indexes = new int[polyList.size()][3];

            int pointIndex = 0;
            for (Point3D.Double p3D : point3DList)
            {
                vertices[pointIndex][0] = p3D.x;
                vertices[pointIndex][1] = p3D.y;
                vertices[pointIndex][2] = p3D.z;
                pointIndex++;
            }

            int polyIndex = 0;
            for (Poly3D poly : polyList)
            {
                indexes[polyIndex][0] = poly.p1;
                indexes[polyIndex][1] = poly.p2;
                indexes[polyIndex][2] = poly.p3;
                polyIndex++;
            }

            ((vtkPolyData) polyData).SetPolys(VtkUtil.getCells(polyList.size(), VtkUtil.prepareCells(indexes)));
            ((vtkPointSet) polyData).SetPoints(VtkUtil.getPoints(vertices));

            ((vtkPolyDataMapper) polyMapper).Update();
        }

        @Override
        public void keyPressed(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            if (isActiveFor(canvas))
            {
                // check we can do the action
                if (!(canvas instanceof Canvas3D) && (imagePoint != null))
                {
                    if (isSelected() && !isReadOnly())
                    {
                        ROI2DShape.this.beginUpdate();
                        try
                        {
                            // send event to controls points first
                            for (Anchor2D pt : controlPoints)
                                pt.keyPressed(e, imagePoint, canvas);
                            
                            // specific action for ROI2DShape
                            if (!e.isConsumed())
                            {
                                switch (e.getKeyCode())
                                {
                                    case KeyEvent.VK_DELETE:
                                    case KeyEvent.VK_BACK_SPACE:
                                        // try to remove selected point
                                        if (removeSelectedPoint(canvas))
                                            e.consume();
                                        break;
                                }
                            }
                        }
                        finally
                        {
                            ROI2DShape.this.endUpdate();
                        }
                    }
                }

            }

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

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

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

        @Override
        public void mousePressed(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // send event to controls points first
            if (isActiveFor(canvas))
            {
                // check we can do the action
                if (!(canvas instanceof Canvas3D) && (imagePoint != null))
                {
                    if (isSelected() && !isReadOnly())
                    {
                        ROI2DShape.this.beginUpdate();
                        try
                        {
                            // default anchor action on mouse pressed
                            for (Anchor2D pt : controlPoints)
                                pt.mousePressed(e, imagePoint, canvas);

                            // specific action for this ROI
                            if (!e.isConsumed())
                            {
                                // left button action
                                if (EventUtil.isLeftMouseButton(e))
                                {
                                    // ROI should not be focused to add point (for multi selection)
                                    if (!isFocused())
                                    {
                                        // try to add point first
                                        if (addPoint(imagePoint.toPoint2D(), EventUtil.isControlDown(e)))
                                            e.consume();
                                    }
                                }
                            }
                        }
                        finally
                        {
                            ROI2DShape.this.endUpdate();
                        }
                    }
                }
            }

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

        @Override
        public void mouseReleased(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // send event to controls points first
            if (isActiveFor(canvas))
            {
                // check we can do the action
                if (!(canvas instanceof Canvas3D) && (imagePoint != null))
                {
                    if (isSelected() && !isReadOnly())
                    {
                        ROI2DShape.this.beginUpdate();
                        try
                        {
                            // default anchor action on mouse release
                            for (Anchor2D pt : controlPoints)
                                pt.mouseReleased(e, imagePoint, canvas);
                        }
                        finally
                        {
                            ROI2DShape.this.endUpdate();
                        }
                    }
                }
            }

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

        @Override
        public void mouseClick(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // send event to controls points first
            if (isActiveFor(canvas))
            {
                // check we can do the action
                if (!(canvas instanceof Canvas3D) && (imagePoint != null))
                {
                    if (isSelected() && !isReadOnly())
                    {
                        ROI2DShape.this.beginUpdate();
                        try
                        {
                            // default anchor action on mouse click
                            for (Anchor2D pt : controlPoints)
                                pt.mouseClick(e, imagePoint, canvas);
                        }
                        finally
                        {
                            ROI2DShape.this.endUpdate();
                        }
                    }
                }
            }

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

            // and process ROI stuff now
            if (isActiveFor(canvas))
            {
                // check we can do the action
                if (!(canvas instanceof Canvas3D) && (imagePoint != null))
                {
                    // not yet consumed...
                    if (!e.isConsumed())
                    {
                        // single click
                        if (e.getClickCount() == 1)
                        {
                            // right click action
                            if (EventUtil.isRightMouseButton(e))
                            {
                                // unselect (don't consume event)
                                if (isSelected())
                                    ROI2DShape.this.setSelected(false);
                            }
                        }
                    }
                }
            }
        }

        @Override
        public void mouseDrag(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // send event to controls points first
            if (isActiveFor(canvas))
            {
                // check we can do the action
                if (!(canvas instanceof Canvas3D) && (imagePoint != null))
                {
                    if (isSelected() && !isReadOnly())
                    {
                        ROI2DShape.this.beginUpdate();
                        try
                        {
                            // default anchor action on mouse drag
                            for (Anchor2D pt : controlPoints)
                                pt.mouseDrag(e, imagePoint, canvas);
                        }
                        finally
                        {
                            ROI2DShape.this.endUpdate();
                        }
                    }
                }
            }

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

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

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

        /**
         * Draw the ROI itself
         */
        protected void drawROI(Graphics2D g, Sequence sequence, IcyCanvas canvas)
        {
            if (canvas instanceof IcyCanvas2D)
            {
                // trivial paint optimization
                final boolean shapeVisible = ShapeUtil.isVisible(g, shape);

                // ROI selected ?
                if (shapeVisible && isSelected())
                {
                    final Graphics2D g2 = (Graphics2D) g.create();
                    final AlphaComposite prevAlpha = (AlphaComposite) g2.getComposite();

                    // show content with an alpha factor
                    g2.setComposite(prevAlpha.derive(prevAlpha.getAlpha() * getOpacity()));
                    g2.setColor(getDisplayColor());
                    g2.fill(shape);

                    g2.dispose();
                }

                final Graphics2D g2 = (Graphics2D) g.create();

                if (shapeVisible)
                {
                    if (isSelected())
                    {
                        // just draw plain object shape without border
                        g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke + 1d)));
                        g2.setColor(getDisplayColor());
                        g2.draw(shape);
                    }
                    else
                    {
                        // draw border
                        g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke + 1d)));
                        g2.setColor(Color.black);
                        g2.draw(shape);
                        // draw shape
                        g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke)));
                        g2.setColor(getDisplayColor());
                        g2.draw(shape);
                    }
                }

                // draw from flatten shape as we use it for collision detection
                // ShapeUtil.drawFromPath(getPathIterator(null, 0.1), g);

                if (isSelected() && !isReadOnly())
                {

                    // draw control point if selected
                    for (Anchor2D pt : controlPoints)
                        pt.paint(g2, sequence, canvas);

                    g2.dispose();
                }

                g2.dispose();
            }

            if (canvas instanceof Canvas3D)
            {
                // 3D canvas
                final Canvas3D canvas3d = (Canvas3D) canvas;

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

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

                // need to rebuild 3D data structures ?
                if (needRebuild)
                {
                    rebuild3DPainter(canvas3d);
                    needRebuild = false;
                }
            }
        }

        @Override
        public void paint(Graphics2D g, Sequence sequence, IcyCanvas canvas)
        {
            if (isActiveFor(canvas))
                drawROI(g, sequence, canvas);
        }

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

            final Color color = getColor();
            final Color focusedColor = getFocusedColor();

            beginUpdate();
            try
            {
                for (Anchor2D anchor : controlPoints)
                {
                    anchor.setColor(color);
                    anchor.setSelectedColor(focusedColor);
                }
            }
            finally
            {
                endUpdate();
            }
        }

        @Override
        public vtkProp[] getProps()
        {
            return new vtkProp[] {(vtkProp) actor};
        }
    }

    class Poly3D
    {
        public Poly3D(int p1, int p2, int p3)
        {
            this.p1 = p1;
            this.p2 = p2;
            this.p3 = p3;
        }

        int p1;
        int p2;
        int p3;
    }

    /**
     * ROI shape (in image coordinates)
     */
    protected final Shape shape;
    /**
     * control points
     */
    protected final List<Anchor2D> controlPoints;

    public ROI2DShape(Shape shape)
    {
        super();

        this.shape = shape;
        controlPoints = new ArrayList<Anchor2D>();
    }

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

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

    /**
     * build a new anchor with specified position
     */
    protected Anchor2D createAnchor(double x, double y)
    {
        return createAnchor(new Point2D.Double(x, y));
    }

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

    @Override
    public void setSelected(boolean value)
    {
        // unselected ? --> unselected all control points
        if (!value)
        {
            for (Anchor2D pt : controlPoints)
                pt.setSelected(false);
        }

        super.setSelected(value);
    }

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

    protected Anchor2D getSelectedPoint()
    {
        for (Anchor2D pt : controlPoints)
            if (pt.isSelected())
                return pt;

        return null;
    }

    /**
     * @deprecated Use {@link #getSelectedPoint()} instead.
     */
    @Deprecated
    protected Anchor2D getSelectedControlPoint()
    {
        return getSelectedPoint();
    }

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

    /**
     * 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(Anchor2D pt)
    {
        addPoint(pt, -1);
    }

    /**
     * internal use only
     */
    protected void addPoint(Anchor2D pt, int index)
    {
        pt.addAnchorListener(this);
        pt.addOverlayListener(this);

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

        roiChanged();
    }

    public boolean addPoint(Point2D pos, boolean insert)
    {
        if (!canAddPoint())
            return false;

        // insertion mode
        if (insert)
        {
            final Anchor2D pt = createAnchor(pos);

            // place the new point with closest points
            addPoint(pt, getInsertPointPosition(pos));
            // always select
            pt.setSelected(true);

            return true;
        }

        // creation mode
        if (isCreating())
        {
            final Anchor2D pt = createAnchor(pos);

            // just add the new point at last position
            addPoint(pt);
            // always select
            pt.setSelected(true);

            return true;
        }

        return false;
    }

    /**
     * @deprecated Use {@link #addPoint(Point2D, boolean)} instead.
     */
    @Deprecated
    public boolean addPointAt(Point2D pos, boolean insert)
    {
        return addPoint(pos, insert);
    }

    /**
     * internal use only
     */
    protected boolean removePoint(@SuppressWarnings("unused") IcyCanvas canvas, Anchor2D pt)
    {
        pt.removeOverlayListener(this);
        pt.removeAnchorListener(this);

        controlPoints.remove(pt);

        // empty ROI ? --> remove from all sequence
        if (controlPoints.size() == 0)
            remove();
        else
            roiChanged();

        return true;
    }

    /**
     * internal use only (used for fast clear)
     */
    protected void removeAllPoint()
    {
        for (Anchor2D pt : controlPoints)
        {
            pt.removeOverlayListener(this);
            pt.removeAnchorListener(this);
        }
        controlPoints.clear();
    }

    /**
     * @deprecated Use {@link #removeSelectedPoint(IcyCanvas)} instead.
     */
    @Deprecated
    public boolean removePointAt(IcyCanvas canvas, Point2D imagePoint)
    {
        if (!canRemovePoint())
            return false;

        // first we try to remove selected point
        if (!removeSelectedPoint(canvas))
        {
            // if no point selected, try to select and remove a point at specified position
            if (selectPointAt(canvas, imagePoint))
                return removeSelectedPoint(canvas);

            return false;
        }

        return true;
    }

    /**
     * @deprecated Use {@link #removeSelectedPoint(IcyCanvas)} instead.
     */
    @Deprecated
    protected boolean removeSelectedPoint(IcyCanvas canvas, Point2D imagePoint)
    {
        return removeSelectedPoint(canvas);
    }

    public boolean removeSelectedPoint(IcyCanvas canvas)
    {
        if (!canRemovePoint())
            return false;

        final Anchor2D selectedPoint = getSelectedPoint();

        if (selectedPoint == null)
            return false;

        final int index = controlPoints.indexOf(selectedPoint);

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

        // last control point removed --> delete ROI
        if (controlPoints.size() == 0)
            remove();
        else
        {
            // save the point position
            final Point2D imagePoint = selectedPoint.getPosition();

            // we are using PathAnchor2D ?
            if (selectedPoint instanceof PathAnchor2D)
            {
                final PathAnchor2D selectedPathPoint = (PathAnchor2D) selectedPoint;

                switch (selectedPathPoint.getType())
                {
                // we removed a MOVETO point ?
                    case PathIterator.SEG_MOVETO:
                        // try to set next point to MOVETO state
                        if (index < controlPoints.size())
                        {
                            final PathAnchor2D nextPoint = (PathAnchor2D) controlPoints.get(index);

                            // next point is a CLOSE one ?
                            if (nextPoint.getType() == PathIterator.SEG_CLOSE)
                            {
                                // delete it
                                if (removePoint(canvas, nextPoint))
                                {
                                    // it was the last control point --> delete ROI
                                    if (controlPoints.size() == 0)
                                        remove();
                                }
                            }
                            else
                            {
                                // whatever is next point, set it to MOVETO
                                nextPoint.setType(PathIterator.SEG_MOVETO);
                                nextPoint.setVisible(true);
                            }
                        }
                        break;

                    // we removed a CLOSE point ?
                    case PathIterator.SEG_CLOSE:
                        // try to set previous point to CLOSE state
                        if (index > 0)
                        {
                            final PathAnchor2D prevPoint = (PathAnchor2D) controlPoints.get(index - 1);

                            // next point is a MOVETO one ?
                            if (prevPoint.getType() == PathIterator.SEG_MOVETO)
                            {
                                // delete it
                                if (removePoint(canvas, prevPoint))
                                {
                                    // it was the last control point --> delete ROI
                                    if (controlPoints.size() == 0)
                                        remove();
                                }
                            }
                            else
                            {
                                // whatever is previous point, set it to CLOSE
                                prevPoint.setType(PathIterator.SEG_CLOSE);
                                prevPoint.setVisible(false);
                            }
                        }
                        break;
                }
            }

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

        return true;
    }

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

        return false;
    }

    /**
     * Return total distance of the specified list of points.
     */
    protected double getTotalDistance(List<Point2D> points, boolean connectLastPoint)
    {
        final int size = points.size();
        double result = 0d;

        if (size > 1)
        {
            for (int i = 0; i < size - 1; i++)
                result += points.get(i).distance(points.get(i + 1));

            // add last to first point distance
            if (connectLastPoint)
                result += points.get(size - 1).distance(points.get(0));
        }

        return result;
    }

    /**
     * Return total distance of the specified list of points.
     */
    protected double getTotalDistance(List<Point2D> points)
    {
        // by default the total length need last point connection
        return getTotalDistance(points, true);
    }

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

        for (Anchor2D pt : controlPoints)
            points.add(pt.getPosition());

        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);
            // minimum distance ?
            if (d < minDistance)
            {
                // save index
                minDistance = d;
                result = i;
            }

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

        return result;
    }

    /**
     * Returns true if specified point coordinates overlap the ROI edge.
     */
    @Override
    public boolean isOverEdge(IcyCanvas canvas, double x, double y)
    {
        // use bigger stroke for isOver test for easier intersection
        final double strk = painter.getAdjustedStroke(canvas) * 3;
        final Rectangle2D rect = new Rectangle2D.Double(x - (strk * 0.5), y - (strk * 0.5), strk, strk);
        // use flatten path, intersects on curved shape return incorrect result
        return ShapeUtil.pathIntersects(getPathIterator(null, 0.1), rect);
    }

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

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

        for (Anchor2D pt : controlPoints)
            result.add(pt.getPosition());

        return result;
    }

    @Override
    public PathIterator getPathIterator(AffineTransform at)
    {
        return shape.getPathIterator(at);
    }

    @Override
    public PathIterator getPathIterator(AffineTransform at, double flatness)
    {
        return shape.getPathIterator(at, flatness);
    }

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

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

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

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

    @Override
    public boolean contains(ROI roi)
    {
        if (roi instanceof ROI2DShape)
            // if the union of both ROI equals base ROI then base ROI contains the other one
            return ShapeUtil.union(shape, ((ROI2DShape) roi).shape).equals(new Area(shape));

        return super.contains(roi);
    }

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

    @Override
    public boolean intersects(double x, double y, double w, double h)
    {
        return shape.intersects(x, y, w, h);
    }

    @Override
    public boolean intersects(ROI roi)
    {
        if (roi instanceof ROI2DShape)
            // if the intersection of both ROI is not empty
            return !ShapeUtil.intersect(shape, ((ROI2DShape) roi).shape).isEmpty();

        return super.contains(roi);
    }

    @Override
    public Rectangle2D computeBounds2D()
    {
        return shape.getBounds2D();
    }

    @Override
    protected ROI computeOperation(ROI roi, BooleanOperator op) throws UnsupportedOperationException
    {
        if (roi instanceof ROI2DShape)
        {
            final ROI2DShape roiShape = (ROI2DShape) roi;

            // special case for subtraction
            if (op == null)
                return new ROI2DPath(ShapeUtil.subtract(this, roiShape));
            else if (op == BooleanOperator.AND)
                return new ROI2DPath(ShapeUtil.intersect(this, roiShape));
            else if (op == BooleanOperator.OR)
                return new ROI2DPath(ShapeUtil.union(this, roiShape));
            else if (op == BooleanOperator.XOR)
                return new ROI2DPath(ShapeUtil.exclusiveUnion(this, roiShape));
        }

        return super.computeOperation(roi, op);
    }

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

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

    /**
     * Called when anchor position changed
     */
    @Override
    public void positionChanged(Anchor2D source)
    {
        // anchor(s) position changed --> ROI changed
        roiChanged();
    }

    /**
     * Called when anchor overlay changed
     */
    @Override
    public void overlayChanged(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();
        }
    }

    /**
     * Called when anchor painter changed, provided only for backward compatibility.<br>
     * Don't use it.
     */
    @SuppressWarnings("deprecation")
    @Override
    public void painterChanged(PainterEvent event)
    {
        // ignore it now
    }

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

        // do here global process on ROI change
        switch (event.getType())
        {
            case ROI_CHANGED:
                // refresh shape
                updateShape();
                break;
        }

        super.onChanged(object);
    }
}