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

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.event.InputEvent;
import java.awt.geom.PathIterator;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import icy.canvas.IcyCanvas;
import icy.painter.VtkPainter;
import icy.sequence.Sequence;
import icy.system.thread.ThreadUtil;
import icy.type.point.Point5D;
import icy.type.rectangle.Rectangle3D;
import icy.vtk.IcyVtkPanel;
import icy.vtk.VtkUtil;
import plugins.kernel.canvas.VtkCanvas;
import plugins.kernel.roi.roi2d.ROI2DShape;
import vtk.vtkActor;
import vtk.vtkCellArray;
import vtk.vtkInformation;
import vtk.vtkPoints;
import vtk.vtkPolyData;
import vtk.vtkPolyDataMapper;
import vtk.vtkProp;
import vtk.vtkProperty;

/**
 * Base class defining a generic 3D Shape ROI as a stack of individual 2D Shape ROI.
 * 
 * @author Stephane
 */
public abstract class ROI3DStackShape extends ROI3DStack<ROI2DShape>
{
    public ROI3DStackShape(Class<? extends ROI2DShape> roiClass)
    {
        super(roiClass);
    }

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

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

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

        return false;
    }

    public class ROI3DStackShapePainter extends ROI3DStackPainter 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;

        public ROI3DStackShapePainter()
        {
            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);

            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();
            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 bounds
            final Rectangle3D bounds = getBounds3D();

            // update outline
            VtkUtil.setOutlineBounds(outline, bounds.getMinX() * scaling[0], bounds.getMaxX() * scaling[0],
                    bounds.getMinY() * scaling[1], bounds.getMaxY() * scaling[1], bounds.getMinZ() * scaling[2],
                    bounds.getMaxZ() * scaling[2], canvas);

            // update polydata object
            final List<double[]> point3DList = new ArrayList<double[]>();
            final List<int[]> polyList = new ArrayList<int[]>();
            final double[] coords = new double[6];

            // starting position
            double xm = 0d;
            double ym = 0d;
            double x0 = 0d;
            double y0 = 0d;
            double x1 = 0d;
            double y1 = 0d;
            double xs = scaling[0];
            double ys = scaling[1];
            int ind;

            for (double z = bounds.getMinZ(); z <= bounds.getMaxZ(); z += 1d)
            {
                // get ROI shape for this slice
                final ROI2DShape roi2dShape = getSlice((int) z);

                // no ROI here --> continue
                if (roi2dShape == null)
                    continue;

                final double z0 = (z + 0d) * scaling[2];
                final double z1 = (z + 1d) * scaling[2];

                // use flat path
                final PathIterator path = roi2dShape.getPathIterator(null, 0.5d);

                // build point data
                while (!path.isDone())
                {
                    switch (path.currentSegment(coords))
                    {
                        case PathIterator.SEG_MOVETO:
                            x0 = xm = coords[0] * xs;
                            y0 = ym = coords[1] * ys;
                            break;

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

                            ind = point3DList.size();

                            point3DList.add(new double[] {x0, y0, z0});
                            point3DList.add(new double[] {x1, y1, z0});
                            point3DList.add(new double[] {x0, y0, z1});
                            point3DList.add(new double[] {x1, y1, z1});
                            polyList.add(new int[] {1 + ind, 2 + ind, 0 + ind});
                            polyList.add(new int[] {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 double[] {x0, y0, z0});
                            point3DList.add(new double[] {x1, y1, z0});
                            point3DList.add(new double[] {x0, y0, z1});
                            point3DList.add(new double[] {x1, y1, z1});
                            polyList.add(new int[] {1 + ind, 2 + ind, 0 + ind});
                            polyList.add(new int[] {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];

            ind = 0;
            for (double[] pt3D : point3DList)
                vertices[ind++] = pt3D;

            ind = 0;
            for (int[] poly : polyList)
                indexes[ind++] = poly;

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

            // actor can be accessed in canvas3d for rendering so we need to synchronize access
            vtkPanel.lock();
            try
            {
                // update outline polygon data
                outlineMapper.SetInputData(outline);
                outlineMapper.Update();
                // update polygon data from cell and points
                polyData.SetPolys(vCells);
                polyData.SetPoints(vPoints);
                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);
                // 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
        public void paint(Graphics2D g, Sequence sequence, IcyCanvas canvas)
        {
            if (isActiveFor(canvas))
            {
                if (canvas instanceof VtkCanvas)
                {
                    // 3D canvas
                    final VtkCanvas cnv = (VtkCanvas) canvas;
                    // update reference if needed
                    if (canvas3d.get() != cnv)
                        canvas3d = new WeakReference<VtkCanvas>(cnv);

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

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

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

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

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

                setFocused(focused);

                return focused;
            }

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

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

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

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