/*
 * (c) Copyright: Artenum SARL, 101-103 Boulevard Mac Donald,
 *                75019, Paris, France 2005.
 *                http://www.artenum.com
 *
 * License:
 *
 *  This program is free software; you can redistribute it
 *  and/or modify it under the terms of the Q Public License;
 *  either version 1 of the License.
 *
 *  This program 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 Q Public License for more details.
 *
 *  You should have received a copy of the Q Public License
 *  License along with this program;
 *  if not, write to:
 *    Artenum SARL, 101-103 Boulevard Mac Donald,
 *    75019, PARIS, FRANCE, e-mail: contact@artenum.com
 */
package com.artenum.cassandra.plugin.glyph;

import java.awt.Frame;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;

import vtk.vtkActor;
import vtk.vtkArrowSource;
import vtk.vtkConeSource;
import vtk.vtkCubeSource;
import vtk.vtkCylinderSource;
import vtk.vtkDataObject;
import vtk.vtkDataSet;
import vtk.vtkGlyph3D;
import vtk.vtkGlyphSource2D;
import vtk.vtkLineSource;
import vtk.vtkLookupTable;
import vtk.vtkMaskPoints;
import vtk.vtkPolyDataMapper;
import vtk.vtkScalarBarActor;
import vtk.vtkSphereSource;

import com.artenum.cassandra.pipeline.CassandraObject;
import com.artenum.cassandra.pipeline.Filter;
import com.artenum.cassandra.pipeline.FilterImpl;
import com.artenum.cassandra.pipeline.PipeLineManager;
import com.artenum.cassandra.pipeline.RemoveListener;
import com.artenum.cassandra.pipeline.graph.VtkObjectCellAdapter;
import com.artenum.cassandra.plugin.CassandraPlugInProperty;
import com.artenum.cassandra.plugin.CassandraPlugin;
import com.artenum.cassandra.plugin.PlugInControlUI;
import com.artenum.cassandra.plugin.PluginManager;
import com.artenum.cassandra.plugin.SimplePlugInProperty;

/**
 * <pre>
 * <b>Project ref           :</b> CASSANDRA project
 * <b>Copyright and license :</b> See relevant sections
 * <b>Status                :</b> under development
 * <b>Creation              :</b> 24/06/2010
 * <b>Modification          :</b>
 * <b>Description  :</b>
 *             This class defines the VTK processing pipeline itself
 *             and the initialisation of the contextuel GUI of the plugin.
 * @author        Julien Forest, ARTENUM SARL
 * @version       0.4
 */
public class GlyphPlugin implements CassandraPlugin, RemoveListener {
    // Cassandra elements
    private String name;
    private PipeLineManager pipelineManager;
    private PluginManager pluginManager;
    private JPopupMenu contextualMenu;
    // private int nbUpdate = 0;

    // Contextual control GUI of the plugin
    private GlyphControlUI controlUI;

    // Vtk data
    private vtkDataSet lastDataSet;
    private vtkLookupTable lookupTable;
    private vtkScalarBarActor scalarBar;
    private vtkGlyph3D glyph;
    private vtkPolyDataMapper glyphMapper;
    private vtkActor glyphActor;
    private vtkMaskPoints ptMask;

    private Filter filterImpl;

    // Pipeline VtkObjects
    private CassandraObject outputCassActor;
    private CassandraObject outputCassMapper;
    private CassandraObject outputCassDataset;
    private CassandraObject outputCassLookupTable;
    private CassandraObject outputCassScalarBar;
    private CassandraObject cassFilter;

    public int maskOnRatio = 1;
    public double scalFactor = 1.0;
    public int resolution = 6;

    public enum glyph_source {
        ARROW3D("3D Arrow", 6), SPHERE("Sphere", 8), CONE("Cone", 6), CYLINDER("Cylinder", 6), BOX("Box", 0), ARROW2D(
                "2D Arrow", 0), VERTEX2D("2D Vertex", 0), LINE2D("2D Line", 0);
        private final String display;
        private final int baseResolution; // if = 0: has no resolution

        private glyph_source(final String s, final int resolution) {
            this.display = s;
            this.baseResolution = resolution;
        }

        @Override
        public String toString() {
            return this.display;
        }

        public boolean hasResolution() {
            return this.baseResolution > 0;
        }

        public int getBaseResolution() {
            return this.baseResolution;
        }

    };

    public enum glyph_scale_mode {
        SCALE_VECTOR("By vector magnitude"), SCALE_SCALAR("By scalar value"), SCALE_VECTORCOMPONENT(
                "By vector components"), SCALE_OFF("Do not scale by datas");
        private final String display;

        private glyph_scale_mode(final String s) {
            this.display = s;
        }

        @Override
        public String toString() {
            return this.display;
        }
    }

    public enum glyph_color_mode {
        COLOR_VECTOR("By vector"), COLOR_SCALAR("By scalar value"), COLOR_SCALE("By size");
        private final String display;

        private glyph_color_mode(final String s) {
            this.display = s;
        }

        @Override
        public String toString() {
            return this.display;
        }
    }

    private glyph_source sourceType = GlyphPlugin.glyph_source.ARROW3D;
    private glyph_scale_mode scaleMode = GlyphPlugin.glyph_scale_mode.SCALE_VECTOR;
    private glyph_color_mode colorMode = GlyphPlugin.glyph_color_mode.COLOR_VECTOR;

    public static final String PERSISTENCE_KEY_SOURCE_TYPE = "peristence.key.source.type";

    /**
     * Property key for the persistence scheme.
     */
    public static final String PERSISTENCE_KEY_INPUT_DATASET = "persistence.key.inputVtkDataSet";
    public static final String PERSISTENCE_KEY_INOUT = "persistence.key.insideOut";
    public static final String PERSISTENCE_KEY_OUPUT_DATASET = "persistence.key.outputDataSet";
    public static final String PERSISTENCE_KEY_OUPUT_MAPPER = "persistence.key.outputMapper";
    public static final String PERSISTENCE_KEY_OUPUT_LOOKUP_TABLE = "persistence.key.outputlookUpTable";
    public static final String PERSISTENCE_KEY_OUTPUT_ACTOR = "persistence.key.outputActor";
    public static final String PERSISTENCE_KEY_OUTPUT_SCALAR_BAR = "persistence.key.ouputScalarBar";
    public static final String PERSISTENCE_KEY_CELL_POS_X = "persistence.key.cell.PosX";
    public static final String PERSISTENCE_KEY_CELL_POS_Y = "persistence.key.cell.PosY";

    /**
     * Main constructor and VTK pipeline definition.
     * 
     * @param pipelineManager
     *            is the central pipeline manager of Cassandra.
     * @param pluginManager
     *            is the central list of plugin instance, where a contextual menu access is given.
     */
    public GlyphPlugin(final PipeLineManager pipelineManager, final PluginManager pluginManager, final Frame owner) {
        initPlugIn(pipelineManager, pluginManager, owner);
        initPipeline();
    }

    /**
     * Constructor used in the pipeline importer
     * 
     * @param pipelineManager
     * @param pluginManager
     * @param owner
     * @param initPipeline
     */
    public GlyphPlugin(final PipeLineManager pipelineManager, final PluginManager pluginManager, final Frame owner,
            final boolean initPipeline) {
        initPlugIn(pipelineManager, pluginManager, owner);
        if (initPipeline) {
            initPipeline();
        }
    }

    /**
     * Default constructor as used during the instantiation in Cassandra.
     * 
     * @param pipelineManager
     * @param pluginManager
     * @param owner
     * @param initPipeline
     */
    public GlyphPlugin(final PipeLineManager pipelineManager, final PluginManager pluginManager, final Frame owner,
            final int initPipeline) {
        initPlugIn(pipelineManager, pluginManager, owner);
        if (initPipeline == 1) {
            initPipeline();
        }
    }

    public void initPlugIn(final PipeLineManager pipelineManager, final PluginManager pluginManager, final Frame owner) {
        // Init default variable
        this.name = "3D glyph";
        this.pipelineManager = pipelineManager;
        this.pluginManager = pluginManager;
        this.filterImpl = new FilterImpl();
        this.filterImpl.addRemoveListener(this);

        // Init all the control UI
        this.controlUI = new GlyphControlUI(pipelineManager, this, owner);

        // Init contextual menu
        this.contextualMenu = new JPopupMenu("Glyph filter menu");
        final JMenuItem showControl = new JMenuItem("Show control");
        showControl.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(final ActionEvent e) {
                GlyphPlugin.this.controlUI.setVisible(true);
            }
        });
        this.contextualMenu.add(showControl);
        this.contextualMenu.addSeparator();
        final JMenuItem remove = new JMenuItem("Remove");
        remove.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(final ActionEvent e) {
                remove();
            }
        });
        this.contextualMenu.add(remove);
    }

    /**
     * intialise the processing pipeline it-self. Should be called after the initPlugIn. If its elements (i.e internal
     * vtk components and the post-filter pipeline) are not set before, their are built as new instances and registered
     * into the pipeline.
     */
    public void initPipeline() {

        // sub-sampling in order to limit the CPU/rendering cost
        if (this.ptMask == null) {
            this.ptMask = new vtkMaskPoints();
        }
        // ptMask.SetInputConnection(normals.GetOutputPort())
        this.ptMask.SetOnRatio(this.maskOnRatio);
        this.ptMask.RandomModeOn();

        // build-up the source to symbolize the glyph
        // by default the glyph is symbolize by an arrow
        final vtkArrowSource arrowSource = new vtkArrowSource();

        // the filter itself
        if (this.glyph == null) {
            this.glyph = new vtkGlyph3D();
            this.outputCassDataset = this.pipelineManager.addDataSet(this.glyph.GetOutput(), "Glyph output");
        } else {
            this.outputCassDataset = this.pipelineManager.getCassandraVtkObjectByTrueVtkObject(this.glyph);
        }
        // re connect to the output of ptMask
        this.glyph.SetInputConnection(this.ptMask.GetOutputPort());
        this.glyph.SetSource(arrowSource.GetOutput());
        // glyph.SetOrient(-1);
        this.glyph.SetVectorModeToUseVector();

        // glyph.SetScaleMode(scalingMode);

        this.glyph.SetOrient(1);
        this.glyph.OrientOn();

        this.glyph.SetScaleModeToScaleByVector();
        this.glyph.SetScaleFactor(this.scalFactor);

        // the mapper
        if (this.glyphMapper == null) {
            this.glyphMapper = new vtkPolyDataMapper();
            this.outputCassMapper = this.pipelineManager.addMapper(this.glyphMapper, "Glyph");
        } else {
            this.outputCassMapper = this.pipelineManager.getCassandraVtkObjectByTrueVtkObject(this.glyphMapper);
        }
        this.glyphMapper.SetInput(this.glyph.GetOutput());

        // the actor
        if (this.glyphActor == null) {
            this.glyphActor = new vtkActor();
            this.outputCassActor = this.pipelineManager.addActor(this.glyphActor, "Glyph");
        } else {
            this.outputCassActor = this.pipelineManager.getCassandraVtkObjectByTrueVtkObject(this.glyphActor);
        }
        this.glyphActor.SetMapper(this.glyphMapper);

        // Build lookupTable
        if (this.lookupTable == null) {
            this.lookupTable = new vtkLookupTable();
            this.outputCassLookupTable = this.pipelineManager.addLookupTable(this.lookupTable, "Glyph");
        } else {
            this.outputCassLookupTable = this.pipelineManager.getCassandraVtkObjectByTrueVtkObject(this.lookupTable);
        }
        this.lookupTable.SetHueRange(0.66667, 0);
        this.lookupTable.Build();
        this.glyphMapper.SetLookupTable(this.lookupTable);

        // Scalar bar
        if (this.scalarBar == null) {
            this.scalarBar = new vtkScalarBarActor();
            this.outputCassScalarBar = this.pipelineManager.addScalarBar(this.scalarBar, "Glyph");
        } else {
            this.outputCassScalarBar = this.pipelineManager.getCassandraVtkObjectByTrueVtkObject(this.scalarBar);
        }
        this.scalarBar.SetLookupTable(this.lookupTable);

        // Load the result data in the Cassandra pipeline
        this.cassFilter = this.pipelineManager.addFilter(this.filterImpl, "Glyph");
        // pipelineManager.setActorVisible(pipelineManager.addScalarBar(scalarBar, "Glyph"), true);

        //
        this.cassFilter.getMetaData().put(CassandraObject.POPUP_MENU, getContextualMenu());

        // end of the VTK pipeline
        this.outputCassActor.setValide(false);
    }

    /**
     * Update the data in the defined pipeline
     * 
     */
    public void updateGlyph(final vtkDataSet vtkDataSet, final glyph_source aSourceType,
            final glyph_scale_mode aScaleMode, final glyph_color_mode aColorMode, final boolean orientByNormal) {
        if (vtkDataSet == null) {
            return;
        }
        this.outputCassActor.setValide(true);

        this.ptMask.SetInputConnection(vtkDataSet.GetProducerPort());
        this.ptMask.SetOnRatio(this.maskOnRatio);

        // These lines refresh and "connects" data inside the newly added dataset
        this.ptMask.Update();
        if (this.ptMask.GetOutput().GetCellData().GetNumberOfArrays() > 0) {
            this.ptMask.GetOutput().GetCellData().SetVectors(this.ptMask.GetOutput().GetCellData().GetArray(0));
        } else if (this.ptMask.GetOutput().GetPointData().GetNumberOfArrays() > 0) {
            this.ptMask.GetOutput().GetPointData().SetVectors(this.ptMask.GetOutput().GetPointData().GetArray(0));
        }

        // select and instantiate arrow type
        this.sourceType = aSourceType;
        switch (aSourceType) {
        default:
        case ARROW3D:
            final vtkArrowSource arrowSource = new vtkArrowSource();
            arrowSource.SetShaftResolution(this.resolution);
            arrowSource.SetTipResolution(this.resolution);
            this.glyph.SetSource(arrowSource.GetOutput());
            break;
        case ARROW2D:
            final vtkGlyphSource2D glyph2dArrowsource = new vtkGlyphSource2D();
            glyph2dArrowsource.SetGlyphTypeToArrow();
            glyph2dArrowsource.FilledOff();
            this.glyph.SetSource(glyph2dArrowsource.GetOutput());
            break;
        case BOX:
            final vtkCubeSource cubeSource = new vtkCubeSource();
            this.glyph.SetSource(cubeSource.GetOutput());
            break;
        case CONE:
            final vtkConeSource coneSource = new vtkConeSource();
            coneSource.SetResolution(this.resolution);
            this.glyph.SetSource(coneSource.GetOutput());
            break;
        case CYLINDER:
            final vtkCylinderSource cylinderSource = new vtkCylinderSource();
            cylinderSource.SetResolution(this.resolution);
            this.glyph.SetSource(cylinderSource.GetOutput());
            break;
        case SPHERE:
            final vtkSphereSource sphereSource = new vtkSphereSource();
            sphereSource.SetThetaResolution(this.resolution);
            sphereSource.SetPhiResolution(this.resolution);
            this.glyph.SetSource(sphereSource.GetOutput());
            break;
        case VERTEX2D:
            final vtkGlyphSource2D glyph2ddiamondsource = new vtkGlyphSource2D();
            glyph2ddiamondsource.SetGlyphTypeToCircle();
            glyph2ddiamondsource.FilledOn();
            this.glyph.SetSource(glyph2ddiamondsource.GetOutput());
            break;
        case LINE2D:
            final vtkLineSource lineSource = new vtkLineSource();
            lineSource.Update();
            this.glyph.SetSource(lineSource.GetOutput());
            break;

        }

        // sets scale mode and general factor
        this.glyph.SetScaleFactor(this.scalFactor);
        this.scaleMode = aScaleMode;
        switch (aScaleMode) {
        case SCALE_OFF:
            this.glyph.SetScaleModeToDataScalingOff();
            break;
        case SCALE_SCALAR:
            this.glyph.SetScaleModeToScaleByScalar();
            break;
        default:
        case SCALE_VECTOR:
            this.glyph.SetScaleModeToScaleByVector();
            break;
        case SCALE_VECTORCOMPONENT:
            this.glyph.SetScaleModeToScaleByVectorComponents();
            break;

        }

        // sets the color mode
        this.colorMode = aColorMode;
        switch (aColorMode) {
        case COLOR_SCALAR:
            this.glyph.SetColorModeToColorByScalar();
            break;
        case COLOR_SCALE:
            this.glyph.SetColorModeToColorByScale();
            break;
        default:
        case COLOR_VECTOR:
            this.glyph.SetColorModeToColorByVector();
            break;

        }

        // orient glyph by surface normal or data sets given vector (if any)
        if (orientByNormal) {
            this.glyph.SetVectorModeToUseNormal();
        } else {
            this.glyph.SetVectorModeToUseVector();
        }

        if ((vtkDataSet.GetPointData().GetScalars() != null) || (vtkDataSet.GetCellData().GetScalars() != null)) {
            if (((this.lastDataSet == null) || !this.lastDataSet.equals(vtkDataSet))
                    && this.glyphMapper.GetLookupTable().equals(this.lookupTable)) {
                this.glyphMapper.SetScalarRange(vtkDataSet.GetScalarRange());
                this.lookupTable.SetTableRange(vtkDataSet.GetScalarRange());
                this.lastDataSet = vtkDataSet;
            }
        }

        // Set the default actor visible by default
        if (this.pipelineManager.getActorList().getData().contains(this.outputCassActor)) {
            this.pipelineManager.setActorVisible(this.outputCassActor, true);
        }
        // and we hide the actor corresponding to the source
        this.pipelineManager.hideInputActor(this.outputCassActor, vtkDataSet);

        // Graph
        this.filterImpl.getInputDataSet().clear();
        this.filterImpl.getInputDataSet().add(vtkDataSet);
        this.filterImpl.getOutputDataSet().clear();
        this.filterImpl.getOutputDataSet().add(this.glyph.GetOutput());
        this.pipelineManager.notifyConnectivityChange(this.cassFilter);
    }

    /**
     * Default plugin interface
     */
    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public JPopupMenu getContextualMenu() {
        return this.contextualMenu;
    }

    @Override
    public void remove() {
        this.pluginManager.removePlugin(this);
        this.pipelineManager.removeVtkObject(this.outputCassActor);
        this.pipelineManager.removeVtkObject(this.outputCassMapper);
        this.pipelineManager.removeVtkObject(this.outputCassDataset);
        this.pipelineManager.removeVtkObject(this.cassFilter);
    }

    /**
     * String printed in the plugin manager list
     */
    @Override
    public String toString() {
        return getName();
    }

    @Override
    public Filter getInternalFilter() {
        return this.filterImpl;
    }

    @Override
    public PlugInControlUI getControlUI() {
        return this.controlUI;
    }

    @Override
    public CassandraPlugInProperty getPlugInProperty() {

        // FIXME : should be moved / handle by the event lsitener of the pipeline. Not very clean.
        this.outputCassLookupTable = this.pipelineManager.getCassandraVtkObjectByTrueVtkObject(this.glyphMapper
                .GetLookupTable());
        this.lookupTable = (vtkLookupTable) this.outputCassLookupTable.getVtkObject();
        this.outputCassScalarBar = this.pipelineManager.getVtkObject(this.outputCassLookupTable
                .getOutputConnectivityList().get(0));
        this.scalarBar = (vtkScalarBarActor) this.outputCassScalarBar.getVtkObject();
        //

        final SimplePlugInProperty prop = new SimplePlugInProperty();

        // specific plugin settings
        prop.put(GlyphPlugin.PERSISTENCE_KEY_SOURCE_TYPE, this.sourceType);

        // general connectivity stuff
        prop.put(GlyphPlugin.PERSISTENCE_KEY_INPUT_DATASET, this.glyph.GetInput());
        prop.put(GlyphPlugin.PERSISTENCE_KEY_OUPUT_DATASET, this.glyph.GetOutput());
        prop.put(GlyphPlugin.PERSISTENCE_KEY_OUPUT_MAPPER, this.glyphMapper);
        prop.put(GlyphPlugin.PERSISTENCE_KEY_OUPUT_LOOKUP_TABLE, this.outputCassLookupTable.getVtkObject());
        prop.put(GlyphPlugin.PERSISTENCE_KEY_OUTPUT_ACTOR, this.outputCassActor.getVtkObject());
        prop.put(GlyphPlugin.PERSISTENCE_KEY_OUTPUT_SCALAR_BAR, this.outputCassScalarBar.getVtkObject());

        final Point cellPosition = ((VtkObjectCellAdapter) this.cassFilter.getMetaData().get(CassandraObject.CELL))
                .getPosition();
        prop.put(GlyphPlugin.PERSISTENCE_KEY_CELL_POS_X, cellPosition.x);
        prop.put(GlyphPlugin.PERSISTENCE_KEY_CELL_POS_Y, cellPosition.y);

        return prop;
    }

    @Override
    public void initAndUpdate(final CassandraPlugInProperty prop) {
        vtkDataSet tmpInputDataSet;

        this.glyph = new vtkGlyph3D();

        // connectivity stuff
        this.glyph.SetOutput((vtkDataObject) prop.getSafely(GlyphPlugin.PERSISTENCE_KEY_OUPUT_DATASET, null)); // FIXME
        tmpInputDataSet = (vtkDataSet) prop.getSafely(GlyphPlugin.PERSISTENCE_KEY_INPUT_DATASET,
                (this.glyph.GetInput()));

        this.lookupTable = (vtkLookupTable) prop.getSafely(GlyphPlugin.PERSISTENCE_KEY_OUPUT_LOOKUP_TABLE, null); // FIXME
        this.glyphMapper = (vtkPolyDataMapper) prop.getSafely(GlyphPlugin.PERSISTENCE_KEY_OUPUT_MAPPER, null); // FIXME
        this.glyphActor = (vtkActor) prop.getSafely(GlyphPlugin.PERSISTENCE_KEY_OUTPUT_ACTOR, null); // FIXME
        this.scalarBar = (vtkScalarBarActor) prop.getSafely(GlyphPlugin.PERSISTENCE_KEY_OUTPUT_SCALAR_BAR, null); // FIXME

        this.initPipeline();
        this.controlUI.setSelectedInput(this.pipelineManager.getCassandraVtkObjectByTrueVtkObject(tmpInputDataSet)); // FIXME:
                                                                                                                     // nasty
        this.updateGlyph(tmpInputDataSet, this.sourceType, this.scaleMode, this.colorMode, false);

    }
}
