import org.sunflow.image.Color;
import org.sunflow.math.Point3;
import org.sunflow.math.Vector3;
import org.sunflow.raytracer.RenderOptions;
import org.sunflow.raytracer.Scene;
import org.sunflow.raytracer.Shader;
import org.sunflow.raytracer.Vertex;
import org.sunflow.raytracer.camera.PinholeCamera;
import org.sunflow.raytracer.geometry.Plane;
import org.sunflow.raytracer.geometry.Sphere;
import org.sunflow.raytracer.geometry.Triangle;
import org.sunflow.raytracer.light.DirectionalSpotlight;
import org.sunflow.raytracer.light.PointLight;
import org.sunflow.raytracer.light.TriangleAreaLight;
import org.sunflow.raytracer.shader.DiffuseShader;
import org.sunflow.raytracer.shader.GlassShader;
import org.sunflow.raytracer.shader.MirrorShader;
import org.sunflow.system.Constants;
import org.sunflow.system.Parser;
import org.sunflow.system.ProgressDisplay;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.HashMap;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.filechooser.FileFilter;

class SunflowGUI extends JFrame {
    private Scene scene;
    private RenderOptions options;
    private ConsoleDialog console;
    private ProgressDialog progressDialog;
    private ImagePanel imagePanel;
    private JMenuItem renderItem;

    public SunflowGUI() {
        super("Sunflow v" + Constants.VERSION);
        addWindowListener(new WindowAdapter() {
                public void windowClosing(WindowEvent e) {
                    System.exit(0);
                }
            });

        JMenuBar mainMenu = new JMenuBar();
        JMenu fileMenu = new JMenu("File");
        JMenuItem openItem = new JMenuItem("Open ...");
        openItem.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent evt) {
                    JFileChooser fc = new JFileChooser(".");
                    fc.setFileFilter(new FileFilter() {
                            public String getDescription() {
                                return "Scene File";
                            }

                            public boolean accept(File f) {
                                return (f.isDirectory() || f.getName().endsWith(".sc"));
                            }
                        });

                    int returnVal = fc.showOpenDialog(SunflowGUI.this);
                    if (returnVal == JFileChooser.APPROVE_OPTION) {
                        final String f = fc.getSelectedFile().getAbsolutePath();
                        final boolean closeConsole = !console.isVisible();

                        // create a thread to load the scene to keep the GUI responsive
                        new Thread() {
                                public void run() {
                                    console.showLater();
                                    loadScene(f);
                                    if (closeConsole)
                                        console.hideLater();
                                }
                            }.start();
                    }
                }
            });
        fileMenu.add(openItem);
        JMenuItem quitItem = new JMenuItem("Quit");
        quitItem.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent evt) {
                    System.exit(0);
                }
            });
        fileMenu.add(quitItem);
        mainMenu.add(fileMenu);
        JMenu renderMenu = new JMenu("Render");
        renderItem = new JMenuItem("Render");
        enableRendering(false);
        renderItem.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent evt) {
                    if (scene == null)
                        return;
                    enableRendering(false);
                    progressDialog.setTask("Initializing", 0, 1);
                    progressDialog.show();
                    new Thread() {
                            public void run() {
                                render(new ProgressDisplay() {
                                        private int lastP;
                                        private int min;
                                        private int max;

                                        public void println(String s) {
                                            console.println(s);
                                        }

                                        public void setTask(String s, int min, int max) {
                                            SunflowGUI.this.progressDialog.setTask(s, min, max);
                                            lastP = -1;
                                            this.min = min;
                                            this.max = max;
                                        }

                                        public void update(int curr) {
                                            int p = ((curr - min) * 100) / (max - min);
                                            if (p != lastP) {
                                                lastP = p;
                                                SunflowGUI.this.progressDialog.setProgress(curr);
                                            }
                                        }

                                        public void updateScanLine(int y, int[] rgb) {
                                            imagePanel.updateScanLine(y, rgb);
                                        }

                                        public boolean isCanceled() {
                                            if (progressDialog.isCanceled()) {
                                                console.println("[GUI] *** Rendering aborted by user ***");
                                                return true;
                                            }
                                            return false;
                                        }
                                    });
                            }
                        }.start();
                }
            });
        renderMenu.add(renderItem);
        mainMenu.add(renderMenu);
        JMenu viewMenu = new JMenu("View");
        JMenuItem consoleItem = new JMenuItem("Console ...");
        consoleItem.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent evt) {
                    console.show();
                }
            });
        viewMenu.add(consoleItem);
        mainMenu.add(viewMenu);
        setJMenuBar(mainMenu);

        imagePanel = new ImagePanel();
        setContentPane(imagePanel);

        pack();
        setLocationRelativeTo(null);
        console = new ConsoleDialog(this);
        progressDialog = new ProgressDialog(this);
        scene = null;
        options = null;
    }

    private void enableRendering(final boolean b) {
        SwingUtilities.invokeLater(new Runnable() {
                public final void run() {
                    renderItem.setEnabled(b);
                }
            });
    }

    private void loadScene(String filename) {
        // load scene and options from file
        scene = new Scene();
        options = new RenderOptions();
        Parser p = new Parser(filename);
        HashMap shadersTable = new HashMap();
        console.println("Parsing " + filename + " ...");
        while (true) {
            String token = p.getNextToken();
            if (token == null)
                break;
            if (token.equals("image")) {
                console.println("Reading image settings ...");
                p.getNextToken(); // "{"
                p.getNextToken(); // "aa"
                options.setMinAASamples(p.getNextInt());
                options.setMaxAASamples(p.getNextInt());
                options.setAAThreshold(p.getNextDouble());
                p.getNextToken(); // "show-aa"
                options.setDisplayAASamples(p.getNextBoolean());
                p.getNextToken(); // "output"
                options.setOutputFilename(p.getNextToken());
                p.getNextToken(); // "}"
            } else if (token.equals("lightserver")) {
                console.println("Reading light server settings ...");
                p.getNextToken(); // "{"
                p.getNextToken(); // "shadows"
                options.setTraceShadows(p.getNextBoolean());
                p.getNextToken(); // "direct-samples"
                options.setNumLightSamples(p.getNextInt());
                p.getNextToken(); // "max-depth"
                options.setMaxDepth(p.getNextInt());
                p.getNextToken(); // "gi"
                options.setComputeGI(p.getNextBoolean());
                options.setNumPhotons(p.getNextInt());
                options.setNumGather(p.getNextInt());
                options.setIrradianceCacheTolerance(p.getNextDouble());
                options.setIrradianceCacheSpacing(p.getNextDouble());
                options.setIrradianceSamples(p.getNextInt());
                p.getNextToken(); // "caustics"
                options.setComputeCaustics(p.getNextBoolean());
                options.setPhotonReductionRatio(p.getNextDouble());
                p.getNextToken(); // "}"
            } else if (token.equals("camera")) {
                p.getNextToken(); // "{"
                p.getNextToken(); // "type"
                token = p.getNextToken();
                if (token.equals("pinhole")) {
                    console.println("Reading pinhole camera ...");
                    p.getNextToken(); // "eye"
                    double ex = p.getNextDouble();
                    double ey = p.getNextDouble();
                    double ez = p.getNextDouble();
                    p.getNextToken(); // "target"
                    double tx = p.getNextDouble();
                    double ty = p.getNextDouble();
                    double tz = p.getNextDouble();
                    p.getNextToken(); // "up"
                    double ux = p.getNextDouble();
                    double uy = p.getNextDouble();
                    double uz = p.getNextDouble();
                    p.getNextToken(); // "fov"
                    double fov = p.getNextDouble();
                    p.getNextToken(); // "resolution"
                    int imageWidth = p.getNextInt();
                    int imageHeight = p.getNextInt();
                    scene.addCamera(new PinholeCamera(new Point3(ex, ey, ez), new Point3(tx, ty, tz), new Vector3(ux, uy, uz), fov, imageWidth, imageHeight));
                    imagePanel.resizeImage(imageWidth, imageHeight);
                } else
                    console.println("Unrecognized camera type: " + token);
                p.getNextToken(); // "}"
            } else if (token.equals("shader")) {
                p.getNextToken(); // "{"
                p.getNextToken(); // "name"
                String name = p.getNextToken();
                console.println("Reading shader: " + name + " ...");
                p.getNextToken(); // "type"
                token = p.getNextToken();
                if (token.equals("diffuse")) {
                    token = p.getNextToken(); // "diff"
                    if (token.equals("diff")) {
                        double dr = p.getNextDouble();
                        double dg = p.getNextDouble();
                        double db = p.getNextDouble();
                        shadersTable.put(name, new DiffuseShader(new Color(dr, dg, db)));
                    } else if (token.equals("texture")) {
                        token = p.getNextToken();
                        console.println("Reading texture: " + token + " ...");
                        shadersTable.put(name, new DiffuseShader(token));
                    } else
                        console.println("Unrecognized option in diffuse shader block:" + token);
                } else if (token.equals("mirror")) {
                    p.getNextToken(); // "refl"
                    double rr = p.getNextDouble();
                    double rg = p.getNextDouble();
                    double rb = p.getNextDouble();
                    shadersTable.put(name, new MirrorShader(new Color(rr, rg, rb)));
                } else if (token.equals("glass")) {
                    p.getNextToken(); // "eta"
                    double eta = p.getNextDouble();
                    p.getNextToken(); // "color"
                    double cr = p.getNextDouble();
                    double cg = p.getNextDouble();
                    double cb = p.getNextDouble();
                    shadersTable.put(name, new GlassShader(eta, new Color(cr, cg, cb)));
                } else
                    console.println("Unrecognized shader type: " + token);
                p.getNextToken(); // "}"
            } else if (token.equals("object")) {
                p.getNextToken(); // "{"
                p.getNextToken(); // "shader"
                Shader shader = (Shader) shadersTable.get(p.getNextToken());
                p.getNextToken(); // "type"
                token = p.getNextToken();
                if (token.equals("mesh")) {
                    p.getNextToken(); // "name"
                    console.println("Reading mesh: " + p.getNextToken() + " ...");
                    int numVertices = p.getNextInt();
                    int numTriangles = p.getNextInt();
                    Vertex[] vertices = new Vertex[numVertices];
                    for (int i = 0; i < numVertices; i++) {
                        Vertex v = new Vertex();
                        p.getNextToken(); // "v"
                        v.p.x = p.getNextDouble();
                        v.p.y = p.getNextDouble();
                        v.p.z = p.getNextDouble();
                        v.n.x = p.getNextDouble();
                        v.n.y = p.getNextDouble();
                        v.n.z = p.getNextDouble();
                        v.tex.x = p.getNextDouble();
                        v.tex.y = p.getNextDouble();
                        vertices[i] = v;
                    }
                    for (int i = 0; i < numTriangles; i++) {
                        p.getNextToken(); // "t"
                        int v0 = p.getNextInt();
                        int v1 = p.getNextInt();
                        int v2 = p.getNextInt();
                        scene.addObject(new Triangle(shader, vertices[v0], vertices[v1], vertices[v2]));
                    }
                } else if (token.equals("flat-mesh")) {
                    p.getNextToken(); // "name"
                    console.println("Reading flat mesh: " + p.getNextToken() + " ...");
                    int numVertices = p.getNextInt();
                    int numTriangles = p.getNextInt();
                    Vertex[] vertices = new Vertex[numVertices];
                    for (int i = 0; i < numVertices; i++) {
                        Vertex v = new Vertex();
                        p.getNextToken(); // "v"
                        v.p.x = p.getNextDouble();
                        v.p.y = p.getNextDouble();
                        v.p.z = p.getNextDouble();
                        v.n = null;
                        p.getNextDouble();
                        p.getNextDouble();
                        p.getNextDouble();
                        v.tex.x = p.getNextDouble();
                        v.tex.y = p.getNextDouble();
                        vertices[i] = v;
                    }
                    for (int i = 0; i < numTriangles; i++) {
                        p.getNextToken(); // "t"
                        int v0 = p.getNextInt();
                        int v1 = p.getNextInt();
                        int v2 = p.getNextInt();
                        scene.addObject(new Triangle(shader, vertices[v0], vertices[v1], vertices[v2]));
                    }
                } else if (token.equals("sphere")) {
                    console.println("Reading sphere ...");
                    p.getNextToken(); // "c"
                    double cx = p.getNextDouble();
                    double cy = p.getNextDouble();
                    double cz = p.getNextDouble();
                    p.getNextToken(); // "r"
                    double r = p.getNextDouble();
                    scene.addObject(new Sphere(shader, new Point3(cx, cy, cz), r));
                } else if (token.equals("plane")) {
                    console.println("Reading plane ...");
                    p.getNextToken(); // "p"
                    double px = p.getNextDouble();
                    double py = p.getNextDouble();
                    double pz = p.getNextDouble();
                    p.getNextToken(); // "n"
                    double nx = p.getNextDouble();
                    double ny = p.getNextDouble();
                    double nz = p.getNextDouble();
                    scene.addObject(new Plane(shader, new Point3(px, py, pz), new Vector3(nx, ny, nz)));
                } else
                    console.println("Unrecognized object type: " + token);
                p.getNextToken(); // "}"
            } else if (token.equals("light")) {
                p.getNextToken(); // "{"
                p.getNextToken(); // "type"
                token = p.getNextToken();
                if (token.equals("mesh")) {
                    p.getNextToken(); // "name"
                    console.println("Reading light mesh: " + p.getNextToken() + " ...");
                    p.getNextToken(); // "emit"
                    Color e = new Color();
                    double er = p.getNextDouble();
                    double eg = p.getNextDouble();
                    double eb = p.getNextDouble();
                    e.set(er, eg, eb);
                    int numVertices = p.getNextInt();
                    int numTriangles = p.getNextInt();
                    Vertex[] vertices = new Vertex[numVertices];
                    for (int i = 0; i < numVertices; i++) {
                        Vertex v = new Vertex();
                        p.getNextToken(); // "v"
                        v.p.x = p.getNextDouble();
                        v.p.y = p.getNextDouble();
                        v.p.z = p.getNextDouble();
                        v.n.x = p.getNextDouble();
                        v.n.y = p.getNextDouble();
                        v.n.z = p.getNextDouble();
                        v.tex.x = p.getNextDouble();
                        v.tex.y = p.getNextDouble();
                        vertices[i] = v;
                    }
                    for (int i = 0; i < numTriangles; i++) {
                        p.getNextToken(); // "t"
                        int v0 = p.getNextInt();
                        int v1 = p.getNextInt();
                        int v2 = p.getNextInt();
                        TriangleAreaLight t = new TriangleAreaLight(vertices[v0], vertices[v1], vertices[v2], e);
                        scene.addLight(t);
                        scene.addObject(t);
                    }
                } else if (token.equals("point")) {
                    console.println("Reading point light ...");
                    p.getNextToken(); // "power"
                    double pr = p.getNextDouble();
                    double pg = p.getNextDouble();
                    double pb = p.getNextDouble();
                    p.getNextToken(); // "p"
                    double px = p.getNextDouble();
                    double py = p.getNextDouble();
                    double pz = p.getNextDouble();
                    scene.addLight(new PointLight(new Point3(px, py, pz), new Color(pr, pg, pb)));
                } else if (token.equals("directional")) {
                    console.println("Reading directional light ...");
                    p.getNextToken(); // "source"
                    double sx = p.getNextDouble();
                    double sy = p.getNextDouble();
                    double sz = p.getNextDouble();
                    p.getNextToken(); // "target"
                    double tx = p.getNextDouble();
                    double ty = p.getNextDouble();
                    double tz = p.getNextDouble();
                    p.getNextToken(); // "radius"
                    double r = p.getNextDouble();
                    p.getNextToken(); // "emit"
                    double er = p.getNextDouble();
                    double eg = p.getNextDouble();
                    double eb = p.getNextDouble();
                    scene.addLight(new DirectionalSpotlight(new Point3(sx, sy, sz), new Point3(tx, ty, tz), r, new Color(er, eg, eb)));
                } else
                    console.println("Unrecognized object type: " + token);
                p.getNextToken(); // "}"
            } else
                console.println("Unrecognized token " + token);
        }

        //        options.setDisplayIrradianceSamples(true);
        console.println("Done parsing.");
        enableRendering(true);
    }

    private void render(ProgressDisplay output) {
        scene.render(options, output);
        progressDialog.hideLater();
        enableRendering(true);
    }

    private static final class ImagePanel extends JPanel {
        private BufferedImage image;

        private ImagePanel() {
            setPreferredSize(new Dimension(640, 480));
            image = null;
        }

        private void resizeImage(final int width, final int height) {
            SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
                        repaint();
                    }
                });
        }

        private void updateScanLine(final int y, final int[] rgb) {
            SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        if ((image == null) || (rgb == null) || (rgb.length != image.getWidth()))
                            return;

                        for (int x = 0; x < rgb.length; x++)
                            image.setRGB(x, image.getHeight() - 1 - y, rgb[x]);

                        repaint();
                    }
                });
        }

        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (image == null)
                return;

            int x = (getWidth() - image.getWidth()) / 2;
            int y = (getHeight() - image.getHeight()) / 2;
            int x0 = x - 1;
            int y0 = y - 1;
            int x1 = x + image.getWidth() + 1;
            int y1 = y + image.getHeight() + 1;
            g.setColor(java.awt.Color.WHITE);
            g.drawLine(x0, y0, x1, y0);
            g.drawLine(x1, y0, x1, y1);
            g.drawLine(x1, y1, x0, y1);
            g.drawLine(x0, y1, x0, y0);
            g.drawImage(image, x, y, this);
        }
    }

    private static final class ConsoleDialog extends JDialog {
        private JTextArea textArea;

        private ConsoleDialog(SunflowGUI parent) {
            super(parent, "Console");
            getContentPane().setLayout(new BorderLayout());

            textArea = new JTextArea(10, 60);
            textArea.setEditable(false);
            textArea.setFocusable(false);
            textArea.setFont(new Font("Monospaced", Font.PLAIN, 12));
            JScrollPane scrollPane = new JScrollPane(textArea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
            scrollPane.setViewportBorder(BorderFactory.createLoweredBevelBorder());
            scrollPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 0, 10));
            getContentPane().add(scrollPane, BorderLayout.CENTER);

            JPanel buttonPane = new JPanel();
            buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.X_AXIS));
            buttonPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
            buttonPane.add(Box.createHorizontalGlue());
            JButton clearButton = new JButton("Clear");
            clearButton.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent evt) {
                        textArea.setText(null);
                    }
                });
            buttonPane.add(clearButton);
            buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
            JButton closeButton = new JButton("Close");
            closeButton.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent evt) {
                        ConsoleDialog.this.hide();
                    }
                });
            buttonPane.add(closeButton);
            getContentPane().add(buttonPane, BorderLayout.SOUTH);

            addWindowListener(new WindowAdapter() {
                    public void windowClosing(WindowEvent e) {
                        ConsoleDialog.this.hide();
                    }
                });

            pack();
            setLocationRelativeTo(parent);
        }

        private void println(final String s) {
            SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        textArea.append(s + "\n");
                    }
                });
        }

        private void showLater() {
            SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        show();
                    }
                });
        }

        private void hideLater() {
            SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        hide();
                    }
                });
        }
    }

    private static final class ProgressDialog extends JDialog {
        private JProgressBar progressBar;
        private String task;
        private volatile boolean cancel;
        private static final long serialVersionUID=new JProgressBar;

        private ProgressDialog(SunflowGUI parent) {
            super(parent, "Rendering Progress", false);
            getContentPane().setLayout(new BorderLayout());

            progressBar = new JProgressBar();
            progressBar.setPreferredSize(new Dimension(256, 24));
            progressBar.setStringPainted(true);
            JPanel barPane = new JPanel();
            barPane.add(progressBar);
            barPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 0, 10));
            getContentPane().add(barPane, BorderLayout.CENTER);

            JPanel buttonPane = new JPanel();
            buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.X_AXIS));
            buttonPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
            buttonPane.add(Box.createHorizontalGlue());
            JButton cancelButton = new JButton("Cancel");
            cancelButton.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent evt) {
                        cancel = true;
                        ProgressDialog.this.hide();
                    }
                });
            buttonPane.add(cancelButton);
            getContentPane().add(buttonPane, BorderLayout.SOUTH);

            addWindowListener(new WindowAdapter() {
                    public void windowClosing(WindowEvent e) {
                        cancel = true;
                        ProgressDialog.this.hide();
                    }
                });

            pack();
            setResizable(false);
            setLocationRelativeTo(parent);
        }

        public void show() {
            super.show();
            cancel = false;
        }

        private void setTask(final String s, final int min, final int max) {
            SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        task = s;
                        progressBar.setString(task);
                        progressBar.setMinimum(min);
                        progressBar.setMaximum(max);
                        progressBar.setValue(min);
                    }
                });
        }

        private void setProgress(final int curr) {
            SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        progressBar.setValue(curr);
                        progressBar.setString(task + " [" + (int) (100.0 * progressBar.getPercentComplete()) + "%]");
                    }
                });
        }

        private boolean isCanceled() {
            return cancel;
        }

        private void hideLater() {
            SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        hide();
                    }
                });
        }
    }

    public static void main(String[] args) {
        SunflowGUI gui = new SunflowGUI();
        if (args.length > 0) {
            System.out.println("Sunflow v" + Constants.VERSION + " textmode");
            System.out.println("Loading scene ...");
            gui.loadScene(args[0]);
            gui.render(null);
            System.exit(0);
        } else
            gui.show();
    }
}