package vtk.sample;

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

import vtk.vtkActor;
import vtk.vtkDataSetMapper;
import vtk.vtkNativeLibrary;
import vtk.vtkObject;
import vtk.vtkPanel;
import vtk.vtkReferenceInformation;
import vtk.vtkRenderer;
import vtk.vtkShrinkFilter;
import vtk.vtkSphereSource;

/**
 * This application run concurrently thread that create VTK objects and the EDT
 * that collect those objects and render them.
 *
 * @author sebastien jourdain - sebastien.jourdain@kitware.com
 */
public class Demo extends JPanel {
    private static final long serialVersionUID = 1L;
    private vtkPanel panel3d;
    private JCheckBox runGC;
    private JCheckBox debugMode;
    private JLabel gcStatus;
    private int NUMBER_OF_PIPLINE_TO_BUILD = 120;
    private int nbSeconds;

    private final CompletionService<vtkActor> exec;

    // -----------------------------------------------------------------
    // Load VTK library and print which library was not properly loaded
    static {
        if (!vtkNativeLibrary.LoadAllNativeLibraries()) {
            for (vtkNativeLibrary lib : vtkNativeLibrary.values()) {
                if (!lib.IsLoaded()) {
                    System.out.println(lib.GetLibraryName() + " not loaded");
                }
            }
        }
        vtkNativeLibrary.DisableOutputWindow(null);
    }

    // -----------------------------------------------------------------
    public static class AddActorRunnable implements Runnable {
        private vtkActor actorToAdd;
        private vtkRenderer renderer;
        private vtkPanel panel;

        void setRenderer(vtkPanel panel) {
            this.renderer = panel.GetRenderer();
            this.panel = panel;
        }

        void setActor(vtkActor a) {
            this.actorToAdd = a;
        }

        public void run() {
            this.renderer.AddActor(this.actorToAdd);
            this.panel.Render();
        }
    }

    // -----------------------------------------------------------------
    public static class PipelineBuilder implements Callable<vtkActor> {
        private vtkActor actor;
        private vtkDataSetMapper mapper;
        private vtkShrinkFilter shrink;
        private vtkSphereSource sphere;

        public vtkActor call() throws Exception {
            // New
            actor = new vtkActor();
            mapper = new vtkDataSetMapper();
            shrink = new vtkShrinkFilter();
            sphere = new vtkSphereSource();

            // Settings
            sphere.SetPhiResolution(60);
            sphere.SetThetaResolution(60);
            double[] center = new double[3];
            sphere.SetCenter(GetRandomCenter(center));
            actor.GetProperty().SetColor(Math.random(), Math.random(), Math.random());

            // Binding
            actor.SetMapper(mapper);
            mapper.SetInputConnection(shrink.GetOutputPort());
            shrink.SetInputConnection(sphere.GetOutputPort());

            // Update
            mapper.Update();

            // Wait some time
            Thread.sleep((long) (Math.random() * 500));

            // Return
            return actor;
        }

        public double[] GetRandomCenter(double[] center) {
            for (int i = 0; i < 3; i++) {
                center[i] = Math.random() * 10;
            }
            return center;
        }
    }

    // -----------------------------------------------------------------

    public Demo() {
        super(new BorderLayout());
        panel3d = new vtkPanel();
        gcStatus = new JLabel("");
        runGC = new JCheckBox("Enable GC", false);
        debugMode = new JCheckBox("Debug mode", false);
        exec = new ExecutorCompletionService<vtkActor>(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()));

        // Setup UI
        JPanel statusBar = new JPanel();
        statusBar.setLayout(new BoxLayout(statusBar, BoxLayout.X_AXIS));
        statusBar.add(runGC);
        statusBar.add(debugMode);
        statusBar.add(Box.createHorizontalGlue());
        statusBar.add(gcStatus);
        add(panel3d, BorderLayout.CENTER);
        add(statusBar, BorderLayout.SOUTH);

        // Init app
        // this.setupGC(); // We use a Swing timer that show the result in UI
        // instead..
        this.setupWorkers();

        // Update GC info into the UI every seconds
        // Reset camera each seconds the first 10 ones
        this.nbSeconds = 0;
        new Timer(1000, new ActionListener() {

            public void actionPerformed(ActionEvent e) {
                if (nbSeconds++ < 10) {
                    panel3d.resetCamera();
                }
                vtkRenderer renderer = panel3d.GetRenderer();
                if (renderer.GetNumberOfPropsRendered() > 1) {
                    renderer.RemoveActor(renderer.GetActors().GetLastProp());
                }

                // Run GC in local thread (EDT)
                if (runGC.isSelected()) {
                    vtkReferenceInformation info = vtkObject.JAVA_OBJECT_MANAGER.gc(debugMode.isSelected());
                    if (debugMode.isSelected()) {
                        System.out.println(info.listKeptReferenceToString());
                        System.out.println(info.listRemovedReferenceToString());
                    }
                    gcStatus.setText(info.toString());
                } else {
                    gcStatus.setText("");
                }

                panel3d.Render();
            }
        }).start();
    }

    private void setupGC() {
        // Setup GC to run every 1 second in EDT
        vtkObject.JAVA_OBJECT_MANAGER.getAutoGarbageCollector().SetScheduleTime(1, TimeUnit.SECONDS);

        // Start/Stop the GC based on the checkbox
        runGC.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent arg0) {
                vtkObject.JAVA_OBJECT_MANAGER.getAutoGarbageCollector().SetAutoGarbageCollection(runGC.isSelected());
            }
        });

        // Change GC mode based on the checkbox
        debugMode.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent arg0) {
                vtkObject.JAVA_OBJECT_MANAGER.getAutoGarbageCollector().SetDebug(debugMode.isSelected());
            }
        });
    }

    private void setupWorkers() {
        // Add actor thread: Consume the working queue and add the actor into
        // the render inside the EDT thread
        final AddActorRunnable adderRunnable = new AddActorRunnable();
        adderRunnable.setRenderer(panel3d);
        new Thread() {
            public void run() {
                for (int i = 0; i < NUMBER_OF_PIPLINE_TO_BUILD; i++) {
                    try {
                        adderRunnable.setActor(exec.take().get());
                        SwingUtilities.invokeAndWait(adderRunnable);
                        panel3d.repaint();
                    } catch (InterruptedException e) {
                        return;
                    } catch (ExecutionException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                }
            };
        }.start();
    }

    public void startWorking() {
        for (int i = 0; i < NUMBER_OF_PIPLINE_TO_BUILD; i++) {
            exec.submit(new PipelineBuilder());
        }
    }

    // -----------------------------------------------------------------
    public static void main(String[] args) throws InterruptedException, InvocationTargetException {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                Demo app = new Demo();

                JFrame f = new JFrame("Concurrency test");
                f.getContentPane().setLayout(new BorderLayout());
                f.getContentPane().add(app, BorderLayout.CENTER);
                f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                f.setSize(400, 400);
                f.setVisible(true);
                f.validate();

                app.startWorking();
            }
        });
    }
}
