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

import icy.preferences.CanvasPreferences;
import icy.system.thread.ThreadUtil;
import icy.util.EventUtil;

import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;

import vtk.vtkActor;
import vtk.vtkActorCollection;
import vtk.vtkAxesActor;
import vtk.vtkCamera;
import vtk.vtkCellPicker;
import vtk.vtkLight;
import vtk.vtkPicker;
import vtk.vtkProp;
import vtk.vtkRenderer;

/**
 * Icy custom VTK panel used for VTK rendering.
 * 
 * @author stephane dallongeville
 */
public class IcyVtkPanel extends VtkJoglPanel implements MouseListener, MouseMotionListener, MouseWheelListener,
        KeyListener, Runnable
{
    /**
     * 
     */
    private static final long serialVersionUID = -8455671369400627703L;

    protected Thread renderingMonitor;
    // protected vtkPropPicker picker;
    protected vtkCellPicker picker;
    protected vtkAxesActor axis;
    protected vtkRenderer axisRenderer;
    protected vtkCamera axisCam;
    protected int axisOffset[];
    protected double axisScale;
    protected boolean lightFollowCamera;
    protected volatile long fineRenderingTime;

    public IcyVtkPanel()
    {
        super();

        // picker
        // picker = new vtkPropPicker();
        // picker.PickFromListOff();

        picker = new vtkCellPicker();
        picker.PickFromListOff();

        // set ambient color to white
        lgt.SetAmbientColor(1d, 1d, 1d);
        lightFollowCamera = true;

        // assign default renderer to layer 0 (should be the case by default)
        ren.SetLayer(0);

        // initialize axis
        axisRenderer = new vtkRenderer();
        // BUG: with OpenGL window the global render window viewport is limited to the last layer viewport dimension
        // axisRenderer.SetViewport(0.0, 0.0, 0.2, 0.2);
        axisRenderer.SetLayer(1);
        axisRenderer.InteractiveOff();

        rw.AddRenderer(axisRenderer);
        rw.SetNumberOfLayers(2);

        axisCam = axisRenderer.GetActiveCamera();

        axis = new vtkAxesActor();
        axisRenderer.AddActor(axis);

        // default axis offset and scale
        axisOffset = new int[] {124, 124};
        axisScale = 1;

        // reset camera
        axisCam.SetViewUp(0, -1, 0);
        axisCam.Elevation(210);
        axisCam.SetParallelProjection(1);
        axisRenderer.ResetCamera();
        axisRenderer.ResetCameraClippingRange();

        // used for restore quality rendering after a given amount of time
        fineRenderingTime = 0;
        renderingMonitor = new Thread(this, "VTK panel rendering monitor");
        renderingMonitor.start();

        addMouseListener(this);
        addMouseMotionListener(this);
        addMouseWheelListener(this);
        addKeyListener(this);
    }

    @Override
    protected void delete()
    {
        // stop thread
        fineRenderingTime = 0;
        renderingMonitor.interrupt();

        super.delete();

        lock.lock();
        try
        {
            // release VTK objects
            axisCam = null;
            axis = null;
            axisRenderer = null;
            picker = null;

            // call it once in parent as this can take a lot fo time
            // vtkObjectBase.JAVA_OBJECT_MANAGER.gc(false);
        }
        finally
        {
            // removing the renderWindow is let to the superclass
            // because in the very special case of an AWT component
            // under Linux, destroying renderWindow crashes.
            lock.unlock();
        }
    }

    @Override
    public void removeNotify()
    {
        // cancel fine rendering request
        fineRenderingTime = 0;

        super.removeNotify();
    }

    @Override
    public void sizeChanged()
    {
        super.sizeChanged();

        updateAxisView();
    }

    /**
     * Return picker object.
     */
    public vtkPicker getPicker()
    {
        return picker;
    }

    /**
     * Return the actor for axis orientation display.
     */
    public vtkAxesActor getAxesActor()
    {
        return axis;
    }

    public boolean getLightFollowCamera()
    {
        return lightFollowCamera;
    }

    /**
     * Return true if the axis orientation display is enabled
     */
    public boolean isAxisOrientationDisplayEnable()
    {
        return (axis.GetVisibility() == 0) ? false : true;
    }

    /**
     * Returns the offset from border ({X, Y} format) for the axis orientation display
     */
    public int[] getAxisOrientationDisplayOffset()
    {
        return axisOffset;
    }

    /**
     * Returns the scale factor (default = 1) for the axis orientation display
     */
    public double getAxisOrientationDisplayScale()
    {
        return axisScale;
    }

    /**
     * Set to <code>true</code> to automatically update light position to camera position when camera move.
     */
    public void setLightFollowCamera(boolean value)
    {
        lightFollowCamera = value;
    }

    /**
     * Return true if the axis orientation display is enabled
     */
    public void setAxisOrientationDisplayEnable(boolean value)
    {
        axis.SetVisibility(value ? 1 : 0);
        updateAxisView();
    }

    /**
     * Sets the offset from border ({X, Y} format) for the axis orientation display (default = {130, 130})
     */
    public void setAxisOrientationDisplayOffset(int[] value)
    {
        axisOffset = value;
        updateAxisView();
    }

    /**
     * Returns the scale factor (default = 1) for the axis orientation display
     */
    public void setAxisOrientationDisplayScale(double value)
    {
        axisScale = value;
        updateAxisView();
    }

    /**
     * @deprecated Use {@link #pickActor(int, int)} instead
     */
    @Deprecated
    public void pickActor(int x, int y)
    {
        pick(x, y);
    }

    /**
     * Pick object at specified position and return it.
     */
    public vtkProp pick(int x, int y)
    {
        lock();
        try
        {
            picker.Pick(x, rw.GetSize()[1] - y, 0, ren);
        }
        finally
        {
            unlock();
        }

        return picker.GetViewProp();
    }

    /**
     * Translate specified camera view
     */
    public void translateView(vtkCamera c, vtkRenderer r, double dx, double dy)
    {
        // translation mode
        double FPoint[];
        double PPoint[];
        double APoint[] = new double[3];
        double RPoint[];
        double focalDepth;

        lock();
        try
        {
            // get the current focal point and position
            FPoint = c.GetFocalPoint();
            PPoint = c.GetPosition();

            // calculate the focal depth since we'll be using it a lot
            r.SetWorldPoint(FPoint[0], FPoint[1], FPoint[2], 1.0);
            r.WorldToDisplay();
            focalDepth = r.GetDisplayPoint()[2];

            final int[] size = rw.GetSize();
            APoint[0] = (size[0] / 2.0) + dx;
            APoint[1] = (size[1] / 2.0) + dy;
            APoint[2] = focalDepth;
            r.SetDisplayPoint(APoint);
            r.DisplayToWorld();
            RPoint = r.GetWorldPoint();
            if (RPoint[3] != 0.0)
            {
                RPoint[0] = RPoint[0] / RPoint[3];
                RPoint[1] = RPoint[1] / RPoint[3];
                RPoint[2] = RPoint[2] / RPoint[3];
            }

            /*
             * Compute a translation vector, moving everything 1/2 the distance
             * to the cursor. (Arbitrary scale factor)
             */
            c.SetFocalPoint((FPoint[0] - RPoint[0]) / 2.0 + FPoint[0], (FPoint[1] - RPoint[1]) / 2.0 + FPoint[1],
                    (FPoint[2] - RPoint[2]) / 2.0 + FPoint[2]);
            c.SetPosition((FPoint[0] - RPoint[0]) / 2.0 + PPoint[0], (FPoint[1] - RPoint[1]) / 2.0 + PPoint[1],
                    (FPoint[2] - RPoint[2]) / 2.0 + PPoint[2]);
            r.ResetCameraClippingRange();
        }
        finally
        {
            unlock();
        }
    }

    /**
     * Rotate specified camera view
     */
    public void rotateView(vtkCamera c, vtkRenderer r, int dx, int dy)
    {
        lock();
        try
        {
            // rotation mode
            c.Azimuth(dx);
            c.Elevation(dy);
            c.OrthogonalizeViewUp();
            r.ResetCameraClippingRange();
        }
        finally
        {
            unlock();
        }
    }

    /**
     * Zoom current view by specified factor (value < 1d means unzoom while value > 1d mean zoom)
     */
    public void zoomView(vtkCamera c, vtkRenderer r, double factor)
    {
        lock();
        try
        {
            if (c.GetParallelProjection() == 1)
                c.SetParallelScale(c.GetParallelScale() / factor);
            else
            {
                c.Dolly(factor);
                r.ResetCameraClippingRange();
            }
        }
        finally
        {
            unlock();
        }
    }

    /**
     * Translate current camera view
     */
    public void translateView(double dx, double dy)
    {
        translateView(cam, ren, dx, dy);
        // adjust light position
        if (getLightFollowCamera())
            setLightToCameraPosition(lgt, cam);
    }

    /**
     * Rotate current camera view
     */
    public void rotateView(int dx, int dy)
    {
        // rotate world view
        rotateView(cam, ren, dx, dy);
        // adjust light position
        if (getLightFollowCamera())
            setLightToCameraPosition(lgt, cam);
        // update axis camera
        updateAxisView();
    }

    /**
     * Zoom current view by specified factor (negative value means unzoom)
     */
    public void zoomView(double factor)
    {
        // zoom world
        zoomView(cam, ren, factor);
        // update axis camera
        updateAxisView();
    }

    /**
     * Set the specified light at the same position than the specified camera
     */
    public static void setLightToCameraPosition(vtkLight l, vtkCamera c)
    {
        l.SetPosition(c.GetPosition());
        l.SetFocalPoint(c.GetFocalPoint());
    }

    /**
     * Set coarse and fast rendering mode immediately
     * 
     * @see #setCoarseRendering(long)
     * @see #setFineRendering()
     */
    public void setCoarseRendering()
    {
        // cancel pending fine rendering restoration
        fineRenderingTime = 0;

        if (rw.GetDesiredUpdateRate() == 20d)
            return;

        lock();
        try
        {
            // set fast rendering
            rw.SetDesiredUpdateRate(20d);
        }
        finally
        {
            unlock();
        }
    }

    /**
     * Set coarse and fast rendering mode <b>for the specified amount of time</b> (in ms).<br>
     * Setting it to 0 means for always.
     * 
     * @see #setFineRendering(long)
     */
    public void setCoarseRendering(long time)
    {
        // want fast update
        setCoarseRendering();

        if (time > 0)
            fineRenderingTime = System.currentTimeMillis() + time;
    }

    /**
     * Set fine (and possibly slow) rendering mode immediately
     * 
     * @see #setFineRendering(long)
     * @see #setCoarseRendering()
     */
    public void setFineRendering()
    {
        // cancel pending fine rendering restoration
        fineRenderingTime = 0;

        if (rw.GetDesiredUpdateRate() == 0.01)
            return;

        lock();
        try
        {
            // set quality rendering
            rw.SetDesiredUpdateRate(0.01);
        }
        finally
        {
            unlock();
        }
    }

    /**
     * Set fine (and possibly slow) rendering <b>after</b> specified time delay (in ms).<br>
     * Using 0 means we want to immediately switch to fine rendering.
     * 
     * @see #setCoarseRendering(long)
     */
    public void setFineRendering(long delay)
    {
        if (delay > 0)
            fineRenderingTime = System.currentTimeMillis() + delay;
        else
            // set back quality rendering immediately
            setFineRendering();
    }

    /**
     * Update axis display depending the current scene camera view.<br>
     * You should call it after having modified camera settings.
     */
    public void updateAxisView()
    {
        if (!isWindowSet())
            return;

        lock();
        try
        {
            double pos[] = cam.GetPosition();
            double fp[] = cam.GetFocalPoint();
            double viewup[] = cam.GetViewUp();

            // mimic axis camera position to scene camera position
            axisCam.SetPosition(pos);
            axisCam.SetFocalPoint(fp);
            axisCam.SetViewUp(viewup);
            axisRenderer.ResetCamera();

            final int[] size = rw.GetSize();
            // adjust scale
            final double scale = size[1] / 512d;
            // adjust offset
            final int w = (int) (size[0] - (axisOffset[0] * scale));
            final int h = (int) (size[1] - (axisOffset[1] * scale));
            // zoom and translate
            zoomView(axisCam, axisRenderer, axisScale * (axisCam.GetDistance() / 17d));
            translateView(axisCam, axisRenderer, -w, -h);
        }
        finally
        {
            unlock();
        }
    }

    @Override
    public void run()
    {
        while (!Thread.currentThread().isInterrupted())
        {
            // nothing to do
            if (fineRenderingTime == 0)
                ThreadUtil.sleep(1);
            else
            {
                // thread used for restoring fine rendering after a certain amount of time
                if (System.currentTimeMillis() >= fineRenderingTime)
                {
                    // set back quality rendering
                    setFineRendering();
                    // request repaint
                    repaint();
                    // done
                    fineRenderingTime = 0;
                }
                // wait until delay elapsed
                else
                    ThreadUtil.sleep(1);
            }
        }
    }

    @Override
    public void lock()
    {
        // if (!isWindowSet())
        // return;

        super.lock();
    }

    @Override
    public void unlock()
    {
        // if (!isWindowSet())
        // return;

        super.unlock();
    }

    @Override
    public void mouseEntered(MouseEvent e)
    {
        // nothing to do here
    }

    @Override
    public void mouseExited(MouseEvent e)
    {
        // nothing to do here
    }

    @Override
    public void mouseClicked(MouseEvent e)
    {
        if (e.isConsumed())
            return;

        // nothing to do here
    }

    @Override
    public void mousePressed(MouseEvent e)
    {
        if (e.isConsumed())
            return;

        // nothing to do here
    }

    @Override
    public void mouseReleased(MouseEvent e)
    {
        if (e.isConsumed())
            return;

        // nothing to do here
    }

    @Override
    public void mouseMoved(MouseEvent e)
    {
        // just save mouse position
        lastX = e.getX();
        lastY = e.getY();
    }

    @Override
    public void mouseDragged(MouseEvent e)
    {
        // camera not yet defined --> exit
        if (cam == null)
            return;

        if (e.isConsumed())
            return;
        if (ren.VisibleActorCount() == 0)
            return;

        // consume event
        e.consume();

        // want fast update
        setCoarseRendering();
        // abort current rendering
        rw.SetAbortRender(1);

        // get current mouse position
        final int x = e.getX();
        final int y = e.getY();
        int deltaX = (lastX - x);
        int deltaY = (lastY - y);

        // faster movement with control modifier
        if (EventUtil.isControlDown(e))
        {
            deltaX *= 3;
            deltaY *= 3;
        }

        if (EventUtil.isRightMouseButton(e) || (EventUtil.isLeftMouseButton(e) && EventUtil.isShiftDown(e)))
            // translation mode
            translateView(-deltaX * 2, deltaY * 2);
        else if (EventUtil.isMiddleMouseButton(e))
            // zoom mode
            zoomView(Math.pow(1.02, -deltaY));
        else
            // rotation mode
            rotateView(deltaX, -deltaY);

        // save mouse position
        lastX = x;
        lastY = y;

        // request repaint
        repaint();

        // restore quality rendering in 1 second
        setFineRendering(1000);
    }

    @Override
    public void mouseWheelMoved(MouseWheelEvent e)
    {
        // camera not yet defined --> exit
        if (cam == null)
            return;

        if (e.isConsumed())
            return;
        if (ren.VisibleActorCount() == 0)
            return;

        // consume event
        e.consume();

        // want fast update
        setCoarseRendering();
        // abort current rendering
        rw.SetAbortRender(1);

        // get delta
        double delta = e.getWheelRotation() * CanvasPreferences.getMouseWheelSensitivity();
        if (CanvasPreferences.getInvertMouseWheelAxis())
            delta = -delta;

        // faster movement with control modifier
        if (EventUtil.isControlDown(e))
            delta *= 3d;

        zoomView(Math.pow(1.02, delta));

        // request repaint
        repaint();

        // restore quality rendering in 1 second
        setFineRendering(1000);
    }

    @Override
    public void keyTyped(KeyEvent e)
    {
        //
    }

    @Override
    public void keyPressed(KeyEvent e)
    {
        if (e.isConsumed())
            return;
        if (ren.VisibleActorCount() == 0)
            return;

        vtkActorCollection ac;
        vtkActor anActor;
        int i;

        switch (e.getKeyChar())
        {
            case 'r': // reset camera
                resetCamera();
                repaint();
                // consume event
                e.consume();
                break;

            case 'w': // wireframe mode
                lock();
                try
                {
                    ac = ren.GetActors();
                    ac.InitTraversal();
                    for (i = 0; i < ac.GetNumberOfItems(); i++)
                    {
                        anActor = ac.GetNextActor();
                        anActor.GetProperty().SetRepresentationToWireframe();
                    }
                }
                finally
                {
                    unlock();
                }
                repaint();
                // consume event
                e.consume();
                break;

            case 's':
                lock();
                try
                {
                    ac = ren.GetActors();
                    ac.InitTraversal();
                    for (i = 0; i < ac.GetNumberOfItems(); i++)
                    {
                        anActor = ac.GetNextActor();
                        anActor.GetProperty().SetRepresentationToSurface();
                    }
                }
                finally
                {
                    unlock();
                }
                repaint();
                // consume event
                e.consume();
                break;
        }
    }

    @Override
    public void keyReleased(KeyEvent e)
    {
        if (e.isConsumed())
            return;
    }
}