package plugins.kernel.roi.roi3d;

import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.geom.Line2D;
import java.util.ArrayList;
import java.util.List;

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

import icy.canvas.IcyCanvas;
import icy.common.CollapsibleEvent;
import icy.math.Line3DIterator;
import icy.painter.Anchor3D;
import icy.resource.ResourceUtil;
import icy.roi.ROI;
import icy.roi.ROIEvent;
import icy.sequence.Sequence;
import icy.type.geom.Line3D;
import icy.type.geom.Polyline3D;
import icy.type.point.Point3D;
import icy.type.point.Point5D;
import icy.util.StringUtil;
import icy.util.XMLUtil;
import icy.vtk.IcyVtkPanel;
import plugins.kernel.canvas.VtkCanvas;
import vtk.vtkTubeFilter;

/**
 * 3D Polyline ROI
 * 
 * @author Stephane Dallongeville
 */
public class ROI3DPolyLine extends ROI3DShape
{
    public class ROI3DPolyLinePainter extends ROI3DShapePainter
    {
        // extra VTK 3D objects
        protected vtkTubeFilter tubeFilter;

        public ROI3DPolyLinePainter()
        {
            super();

            // don't create VTK object on constructor
            tubeFilter = null;
        }

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

            // release allocated VTK resources
            if (tubeFilter != null)
                tubeFilter.Delete();
        };

        @Override
        protected void initVtkObjects()
        {
            super.initVtkObjects();

            // init specific tube filter
            tubeFilter = new vtkTubeFilter();
            tubeFilter.SetInputData(polyData);
            tubeFilter.SetRadius(1d);
            tubeFilter.CappingOn();
            tubeFilter.SetNumberOfSides(8);
            // tubeFilter.SidesShareVerticesOff();
            polyMapper.SetInputConnection(tubeFilter.GetOutputPort());
        }

        /**
         * update 3D painter for 3D canvas (called only when VTK is loaded).
         */
        @Override
        protected void rebuildVtkObjects()
        {
            super.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;

            // sub VTK object not yet initialized (it can happen, have to check why ??)
            if (tubeFilter == null)
                return;

            // actor can be accessed in canvas3d for rendering so we need to synchronize access
            vtkPanel.lock();
            try
            {
                // just be sure the tube filter is also up to date
                tubeFilter.Update();
            }
            finally
            {
                vtkPanel.unlock();
            }
        }

        protected void updateVtkTubeRadius()
        {
            // VTK object not yet initialized
            if (actor == null)
                return;

            final VtkCanvas canvas = canvas3d.get();
            // canvas was closed
            if (canvas == null)
                return;

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

            // sub VTK object not yet initialized (it can happen, have to check why ??)
            if (tubeFilter == null)
                return;

            // update tube radius base on canvas scale X and image scale X
            final double radius = canvas.canvasToImageLogDeltaX((int) getStroke()) * scaling[0];

            if (tubeFilter.GetRadius() != radius)
            {
                // actor can be accessed in canvas3d for rendering so we need to synchronize access
                vtkPanel.lock();
                try
                {
                    tubeFilter.SetRadius(radius);
                    tubeFilter.Update();
                }
                finally
                {
                    vtkPanel.unlock();
                }

                // need to repaint
                painterChanged();
            }
        }

        @Override
        public void drawROI(Graphics2D g, Sequence sequence, IcyCanvas canvas)
        {
            super.drawROI(g, sequence, canvas);

            // update VTK tube radius if needed
            if (canvas instanceof VtkCanvas)
                updateVtkTubeRadius();
        }

        @Override
        protected void drawShape(Graphics2D g, Sequence sequence, IcyCanvas canvas, boolean simplified)
        {
            drawShape(g, sequence, canvas, simplified, false);
        }
    }

    /**
     * 
     */
    public ROI3DPolyLine(Point3D pt)
    {
        super(new Polyline3D());

        // add points to list
        final Anchor3D anchor = createAnchor(pt);
        // just add the new point at last position
        addPoint(anchor);
        // always select
        anchor.setSelected(true);

        updatePolyline();

        // set icon
        setIcon(ResourceUtil.ICON_ROI_POLYLINE);
    }

    /**
     * Generic constructor for interactive mode
     */
    public ROI3DPolyLine(Point5D pt)
    {
        this(pt.toPoint3D());
    }

    public ROI3DPolyLine(Polyline3D polyline)
    {
        this(new Point3D.Double());

        setPolyline3D(polyline);
    }

    public ROI3DPolyLine(List<Point3D> points)
    {
        this(new Point3D.Double());

        setPoints(points);
    }

    public ROI3DPolyLine()
    {
        this(new Point3D.Double());
    }

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

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

    public Polyline3D getPolyline3D()
    {
        return (Polyline3D) shape;
    }

    public void setPoints(List<Point3D> pts)
    {
        beginUpdate();
        try
        {
            removeAllPoint();
            for (Point3D pt : pts)
                addNewPoint(pt, false);
        }
        finally
        {
            endUpdate();
        }
    }

    public void setPolyline3D(Polyline3D value)
    {
        beginUpdate();
        try
        {
            removeAllPoint();
            for (int i = 0; i < value.npoints; i++)
                addNewPoint(new Point3D.Double(value.xpoints[i], value.ypoints[i], value.zpoints[i]), false);
        }
        finally
        {
            endUpdate();
        }
    }

    @Override
    protected double getTotalDistance(List<Point3D> points, double factorX, double factorY, double factorZ)
    {
        // for polyline the total length don't need last point connection
        return Point3D.getTotalDistance(points, factorX, factorY, factorZ, false);
    }

    @Override
    public boolean[] getBooleanMask2D(int x, int y, int width, int height, int z, boolean inclusive)
    {
        if ((width <= 0) || (height <= 0))
            return new boolean[0];

        final List<Point3D> points = getPointsInternal();
        final boolean[] result = new boolean[width * height];

        // 2D bounds
        final Rectangle bounds2d = new Rectangle(x, y, width, height);

        for (int i = 1; i < points.size(); i++)
            drawLine3DInBooleanMask2D(bounds2d, result, z, points.get(i - 1), points.get(i));

        return result;
    }

    public static void drawLine3DInBooleanMask2D(Rectangle bounds2d, boolean[] result, int z, Point3D p1, Point3D p2)
    {
        final Line2D l = new Line2D.Double(p1.getX(), p1.getY(), p2.getX(), p2.getY());

        // 2D intersection ?
        if (l.intersects(bounds2d))
        {
            // 3D intersection ?
            if (((p1.getZ() <= z) && (p2.getZ() >= z)) || ((p2.getZ() <= z) && (p1.getZ() >= z)))
            {
                final int bx = bounds2d.x;
                final int by = bounds2d.y;
                final int pitch = bounds2d.width;
                final Line3DIterator it = new Line3DIterator(new Line3D(p1, p2), 1d);

                while (it.hasNext())
                {
                    final Point3D pt = it.next();

                    // same Z ?
                    if (Math.floor(pt.getZ()) == z)
                    {
                        final int x = (int) Math.floor(pt.getX());
                        final int y = (int) Math.floor(pt.getY());

                        // draw inside the mask
                        if (bounds2d.contains(x, y))
                            result[(x - bx) + ((y - by) * pitch)] = true;
                    }
                }
            }
        }
    }

    /**
     * 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
                updatePolyline();
                break;

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

            case SELECTION_CHANGED:
                final boolean s = isSelected();

                // update controls point state given the selection state of the ROI
                synchronized (controlPoints)
                {
                    for (Anchor3D pt : controlPoints)
                    {
                        pt.setVisible(s);
                        if (!s)
                            pt.setSelected(false);
                    }
                }

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

            default:
                break;
        }

        super.onChanged(object);
    }

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

    @Override
    public boolean contains(ROI roi)
    {
        return false;
    }

    protected void updatePolyline()
    {
        final int len = controlPoints.size();
        final double ptsX[] = new double[len];
        final double ptsY[] = new double[len];
        final double ptsZ[] = new double[len];

        for (int i = 0; i < len; i++)
        {
            final Anchor3D pt = controlPoints.get(i);

            ptsX[i] = pt.getX();
            ptsY[i] = pt.getY();
            ptsZ[i] = pt.getZ();
        }

        final Polyline3D polyline3d = getPolyline3D();

        // we can have a problem here if we try to redraw while we are modifying the polygon points
        synchronized (polyline3d)
        {
            polyline3d.npoints = len;
            polyline3d.xpoints = ptsX;
            polyline3d.ypoints = ptsY;
            polyline3d.zpoints = ptsZ;
            polyline3d.calculateLines();
        }

        // the shape should have been rebuilt here
        ((ROI3DPolyLinePainter) painter).needRebuild = true;
    }

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

            removeAllPoint();

            final ArrayList<Node> nodesPoint = XMLUtil.getChildren(XMLUtil.getElement(node, ID_POINTS), ID_POINT);
            if (nodesPoint != null)
            {
                for (Node n : nodesPoint)
                {
                    final Anchor3D pt = createAnchor(new Point3D.Double());
                    pt.loadPositionFromXML(n);
                    addPoint(pt);
                }
            }
        }
        finally
        {
            endUpdate();
        }

        return true;
    }

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

        final Element dependances = XMLUtil.setElement(node, ID_POINTS);
        for (Anchor3D pt : controlPoints)
            pt.savePositionToXML(XMLUtil.addElement(dependances, ID_POINT));

        return true;
    }
}