/**
 * Copyright (c) Artenum SARL 2004-2005
 * @author Sebastien Jourdain
 *
 * All rights reserved. This software can
 * not be used or copy or diffused without
 * an explicit license of Artenum SARL, Paris-France
 */
package com.artenum.cassandra.renderer.vtk;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.ArrayList;
import java.util.Iterator;

import javax.swing.SwingUtilities;

import vtk.vtkCamera;
import vtk.vtkCanvas;
import vtk.vtkCellPicker;
import vtk.vtkGenericRenderWindowInteractor;
import vtk.vtkInteractorStyleTrackballCamera;
import vtk.vtkJPEGWriter;
import vtk.vtkPNGWriter;
import vtk.vtkPointPicker;
import vtk.vtkTIFFWriter;
import vtk.vtkWindowToImageFilter;

import com.artenum.cassandra.renderer.vtk.PickingObserver;

/**
 * VTK based Cassandra Viewer. Requires the VTK native layer.
 * 
 * @author Sebatien Jourdain, Julien Forest, Benoit Thiebault, Jeremie Turbet
 * 
 * @version 1.1
 */
public class CassandraView extends vtkCanvas implements KeyListener {
    private static final long serialVersionUID = 1L;
    private double zoomFactor = 0.1;
    private boolean deepValidation = true;
    
    private int pickMode = CassandraView.PICK_MODE_TO_NODE;
    private final vtkPointPicker pointPicker;
    private final vtkCellPicker cellPicker;
    private final ArrayList<PickingObserver> pickingObservers;
     
    public static final int PICK_MODE_TO_NONE = -1;
    public static final int PICK_MODE_TO_NODE = 0;
    public static final int PICK_MODE_TO_CELL = 1;
    public static final double DEFAULT_POINT_PICKER_TOLERANCE = 0.05;
    public static final double DEFAULT_CELL_PICKER_TOLERANCE = 0.05;

    /**
     * Main constructor.
     */
    public CassandraView() {
        super();
        
        //management of the 3D pickers
        this.pointPicker = new vtkPointPicker();
        this.pointPicker.SetTolerance(CassandraView.DEFAULT_POINT_PICKER_TOLERANCE);
        
        this.cellPicker = new vtkCellPicker();
        this.cellPicker.SetTolerance(CassandraView.DEFAULT_CELL_PICKER_TOLERANCE);
        
        final vtkInteractorStyleTrackballCamera style = new vtkInteractorStyleTrackballCamera();
        this.getRenderWindowInteractor().SetInteractorStyle(style);
        
        //active the picking mode
        this.setPickMode(pickMode);
        this.pickingObservers = new ArrayList<PickingObserver>();
    }
    
    /**
     * Set the picking mode for the present Cassandra view.
     * @param pickMode - The picking mode as follow:
     * <br>
     * CassandraView.PICK_MODE_TO_NONE (equals to -1): No picker set;
     * <br>
     * CassandraView.PICK_MODE_TO_NODE (equals to 0): Set to points. 
     * <br>
     * CassandraView.PICK_MODE_TO_CELL (equals to 1): Set to cells (default). 
     */
    public void setPickMode(int pickMode){
    	
    	this.pickMode = pickMode;
    	
    	switch (this.pickMode) {
    	
    	case CassandraView.PICK_MODE_TO_NONE:
			this.getRenderWindowInteractor().SetPicker(null);
			this.pointPicker.RemoveAllObservers();
			this.cellPicker.RemoveAllObservers();
			break;
    	
		case CassandraView.PICK_MODE_TO_NODE:
			this.getRenderWindowInteractor().SetPicker(this.pointPicker);
			
			// link the present observer (i.e. this class) to the picker
	        // the used event is EndPickEvent
			this.pointPicker.AddObserver("EndPickEvent", this, "pick");
			break;
			
		case CassandraView.PICK_MODE_TO_CELL:
			this.getRenderWindowInteractor().SetPicker(this.cellPicker);
			
			// link the present observer (i.e. this class) to the picker
	        // the used event is EndPickEvent
			this.cellPicker.AddObserver("EndPickEvent", this, "pick");
			break;

		default:
			break;
		}
    }
    

    /**
     * Get the picking mode (point or cell)
     * @return - the pciking mode. 
     */
    public int getPickingMode(){
    	return this.pickMode;
    }
    
    /**
     * Add a picking observer list to the current CassandrView.
     * See ShowPickingInformation plugin for more information.
     * 
     * @param po - picking observer. 
     */
    public void addPickingObserver(final PickingObserver po) {
        this.pickingObservers.add(po);
    }

    /**
     * Remove the given observer.
     * <br>
     * @param po - remove the given observer. 
     */
    public void removePickingObserver(final PickingObserver po) {
        this.pickingObservers.remove(po);
    }

    public void removePickingObserver(final int index) {
        this.pickingObservers.remove(index);
    }

    /**
     * Get the picking tolerance for the point picker. 
     * @return - tolerance of the point picker. 
     */
    public double getPointPickerTolerance(){
    	return this.pointPicker.GetTolerance();
    }
    
    /**
     * Set the picking tolerance for the point picker.
     * @param tolerance - tolerance of the point picker.
     */
    public void setPointPickerTolerance(double tolerance){
    	this.pointPicker.SetTolerance(tolerance);
    }
    
    /**
     * Get the picking tolerance for the cell picker.
     * @return - Picking tolerance
     */
    public double getCellPickerTolerance(){
    	return this.cellPicker.GetTolerance();
    }
    
    /**
     * Set the picking tolerance for the cell picker.
     * @param tolerance -  Picking tolerance
     */
    public void setCellPickerTolerance(double tolerance){
    	this.cellPicker.SetTolerance(tolerance);
    }
    
    /**
     * Manage the picking action and notifies the registered observers. 
     */
    public void pick() {
    	    	
        PickingObserver observer = null;
        for (final Iterator<PickingObserver> i = this.pickingObservers.iterator(); i.hasNext();) {
            observer = i.next();

            System.out.println("Picked point: " + this.pointPicker.GetPointId());
            // management of the point picker
            if( this.pickMode == CassandraView.PICK_MODE_TO_NODE){
                observer.pickPoint(this.pointPicker);
            }
                
            // management of the cell pointer
            if( this.pickMode == CassandraView.PICK_MODE_TO_CELL ){
                observer.pickCell(this.cellPicker);
            }
        } 
    }

    /**
     * Get the point picker. 
     * @return 
     *         the point picker. 
     */
    public vtkPointPicker getPointPicker() {
        return this.pointPicker;
    }

    /**
     * Set the zoom factor of the view.
     * 
     * @param zoomFactor
     */
    public void setZoomFactor(final double zoomFactor) {
        this.zoomFactor = zoomFactor;
    }

    @Override
    public Dimension getMinimumSize() {
        return new Dimension(0, 0);
    }

    /**
     * Set the position of the camera in the -Z direction in order to see the XY plan.
     */
    public void setXYView() {
        this.setCameraPosition(0.0, 0.0, this.ren.GetActiveCamera().GetDistance(), 0.0);
        this.UpdateLight();
        resetCamera();
    }

    /**
     * Set the position of the camera in the Z direction in order to see the XY plan.
     */
    public void setYXView() {
        this.setCameraPosition(0.0, 0.0, -this.ren.GetActiveCamera().GetDistance(), 0.0);
        this.UpdateLight();
        resetCamera();
    }

    /**
     * set the camera in -x direction to see the YZ plan.
     */
    public void setYZView() {
        this.setCameraPosition(this.ren.GetActiveCamera().GetDistance(), 0.0, 0.0, 0.0);
        this.UpdateLight();
        resetCamera();
    }

    /**
     * set the camera in the x direction to see the ZY plan.
     */
    public void setZYView() {
        this.setCameraPosition(-this.ren.GetActiveCamera().GetDistance(), 0.0, 0.0, 0.0);
        this.UpdateLight();
        resetCamera();
    }

    /**
     * Set the camera position to see the XZ plan. 
     */
    public void setXZView() {
        this.setCameraPosition(0.0, this.ren.GetActiveCamera().GetDistance(), 0.0, 45.0);
        this.UpdateLight();
        resetCamera();
    }

    public void setZXView() {
        this.setCameraPosition(0.0, -this.ren.GetActiveCamera().GetDistance(), 0.0, 45.0);
        this.UpdateLight();
        resetCamera();
    }

    /**
     * Set the position and the roll of the camera at the given position and roll. 
     * 
     * @param xCam
     * @param yCam
     * @param zCam
     * @param rollCam
     */
    protected void setCameraPosition(final double xCam, final double yCam, final double zCam, final double rollCam) {

        System.out.println("Origin: " + this.GetRenderer().GetOrigin()[0] + ", " + this.GetRenderer().GetOrigin()[0]
                + ", " + this.GetRenderer().GetOrigin()[0]);

        this.ren.GetActiveCamera().SetPosition(xCam, yCam, zCam);
        this.ren.GetActiveCamera().SetRoll(rollCam);
    }

    /**
     * Set the back ground color of the renderer.
     */
    @Override
    public void setBackground(final Color c) {
        final double r = ((float) c.getRed()) / 255;
        final double g = ((float) c.getGreen()) / 255;
        final double b = ((float) c.getBlue()) / 255;
        GetRenderer().SetBackground(r, g, b);
    }

    /**
     * Set the ambient light (color) of the renderer.
     * 
     * @param c
     */
    public void setAmbiantLight(final Color c) {
        final double r = ((float) c.getRed()) / 255;
        final double g = ((float) c.getGreen()) / 255;
        final double b = ((float) c.getBlue()) / 255;
        GetRenderer().SetAmbient(r, g, b);
    }

    /**
     * reset the camera position to view the whole scene in the renderer (based on the bounding box of the loadeded
     * object.
     */
    @Override
    public void resetCamera() {
        super.resetCamera();
    }

    /**
     * Zoo the view of the given factor. 
     * 
     * @param zoomFactor - Zoom factor. 
     */
    public void zoom(final double zoomFactor) {
        final vtkCamera cam = GetRenderer().GetActiveCamera();
        if (cam.GetParallelProjection() == 1) {
            cam.SetParallelScale(cam.GetParallelScale() / zoomFactor);
        } else {
            cam.Dolly(zoomFactor);
            resetCameraClippingRange();
        }
    }

    public void rotate(final double xx, final double yy) {
        final vtkCamera cam = GetRenderer().GetActiveCamera();
        cam.Azimuth(xx);
        cam.Elevation(yy);
        cam.OrthogonalizeViewUp();
        resetCameraClippingRange();
        if (this.LightFollowCamera == 1) {
            this.lgt.SetPosition(cam.GetPosition());
            this.lgt.SetFocalPoint(cam.GetFocalPoint());
        }

        System.out.println("Origin: " + this.GetRenderer().GetOrigin()[0] + ", " + this.GetRenderer().GetOrigin()[0]
                + ", " + this.GetRenderer().GetOrigin()[0]);

    }

    public synchronized void deepValidateView() {
        // Linux view validation error
        if (System.getProperty("os.name").toLowerCase().indexOf("win") == -1) {
            Render();
            invalidate();
            validate();
            repaint();
        } else {
            repaint();
        }

        final Dimension d = getPreferredSize();
        if (!getSize().equals(d)) {
            setSize(1, 1);
            setSize(d);
            invalidate();
            validate();
            repaint();
        } else if (this.deepValidation) {
            this.deepValidation = false;
            setSize(1, 1);
            setSize(d);
            invalidate();
            validate();
            repaint();
        }
    }

    public void validateViewAndWait() {
        try {
            SwingUtilities.invokeAndWait(new Runnable() {
                @Override
                public void run() {
                    // Linux view validation error
                    if (System.getProperty("os.name").toLowerCase().indexOf("win") == -1) {
                        Render();
                        invalidate();
                        validate();
                        repaint();
                    } else {
                        repaint();
                    }
                }
            });
        } catch (final Exception e) {
            e.printStackTrace();
        }
    }

    public void validateViewAndGo() {
        try {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    // Linux view validation error
                    if (System.getProperty("os.name").toLowerCase().indexOf("win") == -1) {
                        Render();
                        invalidate();
                        validate();
                        repaint();
                    } else {
                        repaint();
                    }
                }
            });
        } catch (final Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void keyPressed(final KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_I) {
            zoom(1.0 + this.zoomFactor);
        } else if (e.getKeyCode() == KeyEvent.VK_O) {
            zoom(1.0 - this.zoomFactor);
        } else if (e.getKeyCode() == KeyEvent.VK_UP) {
            rotate(0, -1);
        } else if (e.getKeyCode() == KeyEvent.VK_DOWN) {
            rotate(0, 1);
        } else if (e.getKeyCode() == KeyEvent.VK_LEFT) {
            rotate(1, 0);
        } else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
            rotate(-1, 0);
        } else {
            super.keyPressed(e);
        }
    }

    @Override
    public void keyReleased(final KeyEvent e) {
        super.keyReleased(e);
    }

    @Override
    public void keyTyped(final KeyEvent e) {
        super.keyTyped(e);
    }

    /**
     * Save a screenshot of the view into a PNG format file. 
     * 
     * @param fileToSave - output file. 
     */
    public synchronized void saveToPNG(final String fileToSave) {
        Lock();

        // FIXME OffScreen rendering hack for Linux
        final String os = System.getProperty("os.name").toLowerCase();
        final Boolean isLinux = os.contains("linux");

        if (isLinux) {
            this.GetRenderWindow().OffScreenRenderingOn();
        }

        final vtkWindowToImageFilter w2if = new vtkWindowToImageFilter();
        w2if.SetInput(this.rw);

        w2if.SetMagnification(1);
        w2if.Update();

        final vtkPNGWriter writer = new vtkPNGWriter();
        writer.SetInput(w2if.GetOutput());
        writer.SetFileName(fileToSave);
        writer.Write();

        if (isLinux) {
            this.GetRenderWindow().OffScreenRenderingOff();
        }

        UnLock();
    }

    /**
     * Save a screenshot of the view into a JPG format file.
     * 
     * @param fileToSave
     */
    public synchronized void saveToJPG(final String fileToSave) {
        Lock();

        this.GetRenderWindow().OffScreenRenderingOn();
        final vtkWindowToImageFilter w2if = new vtkWindowToImageFilter();
        w2if.SetInput(this.rw);

        w2if.SetMagnification(1);

        this.ren.Render();
        w2if.Update();

        final vtkJPEGWriter writer = new vtkJPEGWriter();
        writer.SetInput(w2if.GetOutput());
        writer.SetFileName(fileToSave);
        writer.Write();

        this.ren.Render();

        this.GetRenderWindow().OffScreenRenderingOff();
        UnLock();
    }

    public synchronized void saveToTIFF(final String fileToSave) {
        Lock();

        this.GetRenderWindow().OffScreenRenderingOn();
        final vtkWindowToImageFilter w2if = new vtkWindowToImageFilter();
        w2if.SetInput(this.rw);

        w2if.SetMagnification(1);

        this.ren.Render();
        w2if.Update();

        final vtkTIFFWriter writer = new vtkTIFFWriter();
        writer.SetInput(w2if.GetOutput());
        writer.SetFileName(fileToSave);
        writer.Write();
        this.ren.Render();

        this.GetRenderWindow().OffScreenRenderingOff();
        UnLock();
    }

    // public vtkGenericRenderWindowInteractor getInteractor() {
    // return getRenderWindowInteractor();
    // }

    @Override
    public vtkGenericRenderWindowInteractor getRenderWindowInteractor() {
        return super.getIren();
    }
}
