/*

CSci 5108 
Final Project
Thomas Burt
Spring 2008


======
Description of Project:

This project was an attempt to extract surface geometery from non-physically based mathematical
systems. However, the approach taken is generally applicable to any system in which vector field
information is available. The hope was to come out with a tool that would make it easier to analyze
some of the models we use when generating 3D data.


===
Running The Source:

For some reason, I decided to use Java for this project. I had hoped to incorporate some UI elements
into the project, and this was the motivating factor. I hope this doesn't complicate things too much,
if I had been thinking clearly I would have stuck with using C. I used the Java OpenGL Bindings
( JOGL ) libraries.  I had a little bit of trouble compiling on the itlabs machines, but..

I did need to download JOGL jar files from https://jogl.dev.java.net. 
There were some more instructions there too, at
https://jogl.dev.java.net/nonav/source/browse/checkout/jogl/doc/userguide/index.html?rev=HEAD&content-type=text/html

*/
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.io.*;
import java.nio.*;
import java.util.*;
import javax.media.opengl.*;
import javax.media.opengl.glu.*;
import com.sun.opengl.util.*;


public class JOGLMain implements GLEventListener, MouseListener, MouseMotionListener {

    private GLU glu;
    GLUquadric qobj;
    private float view_rotx = 20.0f,  view_roty = 30.0f,  view_rotz = 0.0f;
    private int prevMouseX,  prevMouseY;
    private boolean mouseRButtonDown;
    private boolean mouseMButtonDown;
    private Vector eye;
    private Particle average;
    private Particle averageTmp;
    private Trail[] trails;
    private int NUM_TRAILS = 40;

    public JOGLMain() {
        this.average = new Particle();
        this.average.blank();
        this.eye = new Vector(0.0, 25.0, -12.0);
        trails = new Trail[NUM_TRAILS];
        for (int i = 0; i < trails.length; i++) {
            trails[i] = new Trail();
        }
    }

    public static void main(String[] args) {
        Frame frame = new Frame("Attactor Trails");
        GLCanvas canvas = new GLCanvas();
        canvas.addGLEventListener(new JOGLMain());
        frame.add(canvas);
        frame.setSize(600, 600);
        final Animator animator = new Animator(canvas);
        frame.addWindowListener(new WindowAdapter() {

            @Override
            public void windowClosing(WindowEvent e) {
                // Run this on another thread than the AWT event queue to
                // make sure the call to Animator.stop() completes before
                // exiting
                new Thread(new Runnable() {

                    public void run() {
                        animator.stop();
                        System.exit(0);
                    }
                    }).start();
            }
            });

        frame.setVisible(true);
        animator.start();
    }

    public void init(GLAutoDrawable drawable) {

        GL gl = drawable.getGL();
        glu = new GLU();
        qobj = glu.gluNewQuadric();

        //
        gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        gl.glShadeModel(GL.GL_SMOOTH);
        gl.glEnable(GL.GL_DEPTH_TEST);

        drawable.addMouseListener(this);
        drawable.addMouseMotionListener(this);

        System.err.println("GL_VENDOR: " + gl.glGetString(GL.GL_VENDOR));
        System.err.println("GL_RENDERER: " + gl.glGetString(GL.GL_RENDERER));
        System.err.println("GL_VERSION: " + gl.glGetString(GL.GL_VERSION));

    }

    public void display(GLAutoDrawable drawable) {



        for (Trail t : trails) {
            t.update();
        }

        GL gl = drawable.getGL();

        gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);

        gl.glMatrixMode(GL.GL_MODELVIEW);
        gl.glLoadIdentity();
        glu.gluLookAt(
                eye.x, eye.y, eye.z,
                0.0, 0.0, 0.0,
                //                this.average.position.x, this.average.position.y, this.average.position.z, 
                0.0, 1.0, 0.0);

        gl.glPushMatrix();

        gl.glRotatef(view_rotx, 1.0f, 0.0f, 0.0f);
        gl.glRotatef(view_roty, 0.0f, 1.0f, 0.0f);
        gl.glRotatef(view_rotz, 0.0f, 0.0f, 1.0f);


        // a big x,y,z axis
        gl.glPushMatrix();
        gl.glScaled(30.0, 30.0, 30.0);
        gl.glBegin(GL.GL_LINES);
        gl.glColor3d(1.0, 0.0, 0.0);
        gl.glVertex3d(0.0, 0.0, 0.0);
        gl.glVertex3d(1.0, 0.0, 0.0);
        gl.glColor3d(0.0, 1.0, 0.0);
        gl.glVertex3d(0.0, 0.0, 0.0);
        gl.glVertex3d(0.0, 1.0, 0.0);
        gl.glColor3d(0.0, 0.0, 1.0);
        gl.glVertex3d(0.0, 0.0, 0.0);
        gl.glVertex3d(0.0, 0.0, 1.0);
        gl.glEnd();
        gl.glPopMatrix();

        //gl.glDrawElements(GL.GL_TRIANGLES, this.total_trianlges, GL.GL_UNSIGNED_INT, this.indicesBuf);
        gl.glPushMatrix();
        for (Trail t : trails) {
            t.draw(gl);
        }
        gl.glPopMatrix();
        gl.glPopMatrix();


        gl.glFlush();



    }

    public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
        GL gl = drawable.getGL();
        float aspect = (float) height / (float) width;
        gl.glMatrixMode(GL.GL_PROJECTION);
        gl.glLoadIdentity();
//        gl.glOrtho(-50.0, 50.0, -50.0, 50.0, -50.0, 50);
        glu.gluPerspective(60.0, aspect, 1.0, 500.0);
        gl.glMatrixMode(GL.GL_MODELVIEW);
        gl.glLoadIdentity();
        glu.gluLookAt(0.0, 55.0, -22.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);

    }

    public void displayChanged(GLAutoDrawable drawable, boolean modeChanged,
            boolean deviceChanged) {
    }

    // Methods required for the implementation of MouseListener
    public void mouseEntered(MouseEvent e) {
    }

    public void mouseExited(MouseEvent e) {
    }

    public void mousePressed(MouseEvent e) {
        prevMouseX = e.getX();
        prevMouseY = e.getY();
        if ((e.getModifiers() & MouseEvent.BUTTON3_MASK) != 0) {
            mouseRButtonDown = true;
        }
        if ((e.getModifiers() & MouseEvent.BUTTON2_MASK) != 0) {
            mouseMButtonDown = true;
        }
    }

    public void mouseReleased(MouseEvent e) {
        if ((e.getModifiers() & MouseEvent.BUTTON3_MASK) != 0) {
            mouseRButtonDown = false;
        }
        if ((e.getModifiers() & MouseEvent.BUTTON2_MASK) != 0) {
            mouseMButtonDown = false;
        }
    }

    public void mouseClicked(MouseEvent e) {
    }

    // Methods required for the implementation of MouseMotionListener
    public void mouseDragged(MouseEvent e) {
        int x = e.getX();
        int y = e.getY();
        Dimension size = e.getComponent().getSize();

        if (mouseRButtonDown == true) {
            float trnX = 100.0f * ((float) (x - prevMouseX) / (float) size.width);
            float trnY = 100.0f * ((float) (prevMouseY - y) / (float) size.height);
            prevMouseX = x;
            prevMouseY = y;
            eye.x += trnX;
            eye.y += trnY;

        } else if (mouseMButtonDown == true) {
            float trnX = 100.0f * ((float) (x - prevMouseX) / (float) size.width);
            float trnY = 100.0f * ((float) (prevMouseY - y) / (float) size.height);
            prevMouseX = x;
            prevMouseY = y;
            eye.z += (trnX + trnY);


        } else {
            float thetaY = 360.0f * ((float) (x - prevMouseX) / (float) size.width);
            float thetaX = 360.0f * ((float) (prevMouseY - y) / (float) size.height);
            prevMouseX = x;
            prevMouseY = y;

            view_rotx += thetaX;
            view_roty += thetaY;

        }


    }

    public void mouseMoved(MouseEvent e) {
    }

    class Trail {

        private int LENGTH = 90;
        private int WIDTH = 20;
        private Particle[][] slices;
        private int slices_i = 0;
        private double MURP_FACTOR = 0.56;

        public Trail() {


            slices = new Particle[LENGTH][];
            for (int i = 0; i < LENGTH; i++) {
                slices[i] = new Particle[WIDTH];
            }

            // create the first flank of particles
            slices[0][0] = new Particle();
            for (int i = 0; i < WIDTH; i++) {
                double murp = ((double) i) / ((double) WIDTH);
                slices[0][i] = slices[0][0].clone();
                slices[0][i].position = slices[0][0].position.nudge(murp * MURP_FACTOR);
            }

        }

        public void update() {

            for (int i = 0; i < WIDTH; i++) {
                slices[(slices_i + 1) % LENGTH][i] = slices[slices_i][i].update();

            }
            slices_i = (slices_i + 1) % LENGTH;
        //   System.out.println(average.color.toString());


        }

        void draw(GL gl) {
            for (int i = 0; i < WIDTH; i++) {

                gl.glBegin(gl.GL_LINE_STRIP);
                for (int ii = slices_i + 1; (ii % LENGTH) != slices_i; ii++) {
                    if (slices[ii % LENGTH] != null && slices[ii % LENGTH][i] != null) {
                        gl.glColor3d(
                                slices[ii % LENGTH][i].color.red,
                                slices[ii % LENGTH][i].color.green,
                                slices[ii % LENGTH][i].color.blue);
                        gl.glVertex3d(
                                slices[ii % LENGTH][i].position.x,
                                slices[ii % LENGTH][i].position.y,
                                slices[ii % LENGTH][i].position.z);
                    }
                }
                gl.glEnd();
            }

        }
    }

    class Particle implements Cloneable {

        public Vector position;
        public Vector velocity;
        public double mass;
        public Color color;
        public double divergance;
        private double PARTICLE_MASS = 2.75;
        private double MAX_POS_INIT = 35.0;
        private double MAX_POS = 5000.0;
        private double TIMESTEP = 0.6716;
        private double HALFTIME_SQUARED = TIMESTEP * TIMESTEP * 0.5;
        private double XCOS_ARG = -1.2;
        private double XY_ARG = 1.0;
        private double XSIN_ARG = -1.8;
        private double XZ_ARG = 1.0;
        private double YCOS_ARG = 1.1;
        private double YZ_ARG = 1.0;
        private double YSIN_ARG = 0.4;
        private double YX_ARG = -1.0;
        private double ZCOS_ARG = 1.0;
        private double ZX_ARG = -1.8;
        private double ZSIN_ARG = 1.0;
        private double ZY_ARG = 1.0;

        public Particle() {

            this.position = new Vector(
                    ((Math.random() - 0.5) * MAX_POS_INIT),
                    ((Math.random() - 0.5) * MAX_POS_INIT),
                    ((Math.random() - 0.5) * MAX_POS_INIT));
            this.velocity = new Vector(0.0, 0.0, 0.0);
            this.mass = PARTICLE_MASS;
            this.color = new Color(Math.random(), Math.random(), Math.random(), 1.0);
        }

        public void blank() {
            position = new Vector(0.0, 0.0, 0.0);
            velocity = new Vector(0.0, 0.0, 0.0);
            color = new Color(0.0, 0.0, 0.0, 1.0);
            mass = 0.0;
            divergance = 0.0;
        }

        public Particle update() {

            Particle p = this.clone();

            Vector pos0 = this.position.clone();
            Vector nudg = this.position.nudge(this.mass / this.divergance);
            nudg = f(nudg);
            Vector force = f(this.position);
            p.position =
                    this.position.plus(
                    this.velocity.times(TIMESTEP).plus(
                    force.times(1 / this.mass).times(HALFTIME_SQUARED)));
            p.velocity = this.position.plus(pos0.times(-1.0));

            if (this.position.length() > MAX_POS) {
                return new Particle();
            }

            p.divergance = nudg.plus(p.position.times(-1.0)).length();

            return p;
        }

        public Vector f(Vector pos) {

            Vector v;
            /* Rossler
            v = new Vector(
                    -pos.y - pos.z * pos.x,
                    pos.x + 0.7 * pos.y,
                    0.2 + pos.z * (pos.x - 5.7));
            //* */
            /* Lorentz
            v = new Vector(
            -10 * pos.x + 10 * pos.y,
            28 * pos.x - pos.y - pos.x * pos.z,
            -8.0 * pos.z / 3.0 + pos.x * pos.y);
            //*/

            //* Trigonometric
            v = new Vector(
            (XCOS_ARG * Math.cos(XY_ARG * pos.y) + XSIN_ARG * Math.sin(XZ_ARG * pos.z)),
            (YCOS_ARG * Math.cos(YZ_ARG * pos.z) + YSIN_ARG * Math.sin(YX_ARG * pos.x)),
            (ZCOS_ARG * Math.cos(ZX_ARG * pos.x) + ZSIN_ARG * Math.sin(ZY_ARG * pos.y)));
            //v = v.times(Math.pow(Math.E, -1 * v.length()));
            //*/
            return v;
        }

        public void factor(int factor) {
            position = position.times(1.0 / (double) factor);
            velocity = velocity.times(1.0 / (double) factor);
            color = color.times(1.0 / (double) factor);
            mass = mass / (double) factor;
            divergance = divergance / (double) factor;
        }

        public void plus(Particle p) {
            position = position.plus(p.position);
            velocity = velocity.plus(p.velocity);
            color = color.plus(p.color);
            mass = mass + p.mass;
            divergance = divergance + p.divergance;
        }

        @Override
        public Particle clone() {
            try {
                return (Particle) super.clone();
            } catch (CloneNotSupportedException ex) {

            }
            return new Particle();
        }
    }

    class Vector implements Cloneable {

        private double MIN_VECTOR_NUDGE = 0.0025;
        public double x;
        public double y;
        public double z;
        public double v;

        public Vector(double x, double y, double z) {
            this.x = x;
            this.y = y;
            this.z = z;
            this.v = 1.0;
        }

        public Vector plus(Vector v1) {
            return new Vector(
                    v1.x + this.x,
                    v1.y + this.y,
                    v1.z + this.z);
        }

        public Vector times(double d) {
            return new Vector(
                    d * this.x,
                    d * this.y,
                    d * this.z);
        }

        public boolean equals(Vector v) {
            boolean b = false;
            if (v != null) {
                if (Math.abs(v.x - this.x) < MIN_VECTOR_NUDGE ||
                        Math.abs(v.y - this.y) < MIN_VECTOR_NUDGE ||
                        Math.abs(v.z - this.z) < MIN_VECTOR_NUDGE) {
                    b = true;
                }
            }
            return b;
        }

        public Vector cross(Vector v) {
            return new Vector(
                    (this.y * v.z - this.z * v.y),
                    (this.z * v.x - this.x * v.z),
                    (this.x * v.y - this.y * v.x));
        }

        public double dot(Vector v) {
            return (this.x * v.x +
                    this.y * v.y +
                    this.z * v.z);
        }

        public double length() {
            return Math.sqrt(
                    Math.pow(this.x, 2.0) +
                    Math.pow(this.y, 2.0) +
                    Math.pow(this.z, 2.0));
        }

        public Vector normalize() {
            double length = this.length();
            return new Vector(
                    this.x / length,
                    this.y / length,
                    this.z / length);
        }

        public Vector nudge(double amount) {

            return new Vector(
                    this.x + (Math.random() - 0.5) * amount,
                    this.y + (Math.random() - 0.5) * amount,
                    this.z + (Math.random() - 0.5) * amount);
        }

        public Vector nudge(Vector v) {

            return new Vector(
                    this.x + v.x,
                    this.y + v.y,
                    this.z + v.z);

        }

        @Override
        public String toString() {
            String s = "";
            s = "{x,y,z} = " + Double.toString(this.x) + ", " + Double.toString(this.y) + ", " + Double.toString(this.z);
            return s;
        }

        @Override
        public Vector clone() {
            return new Vector(this.x, this.y, this.z);
        }
    }

    class Color {

        public double hue = 1.0;
        public double sat = 1.0;
        public double val = 1.0;
        public double red = 1.0;
        public double green = 1.0;
        public double blue = 1.0;
        public double alpha = 1.0;

        public Color(double red, double green, double blue, double alpha) {
            this.red = red;
            this.green = green;
            this.blue = blue;
            this.alpha = alpha;

        }

        public Color times(double factor) {
            return new Color(
                    this.red / factor,
                    this.green / factor,
                    this.blue / factor,
                    this.alpha / factor);
        }

        public Color plus(Color c) {
            return new Color(
                    this.red + c.red,
                    this.green + c.green,
                    this.blue + c.blue,
                    this.alpha + c.alpha);
        }

        public void nudgeHue(double amount) {

            this.setHSV();
            this.setHSV(hue + amount, sat, val);

        }

        private void setHSV() {
            double max = Math.max(red, Math.max(green, blue));
            double min = Math.min(red, Math.min(green, blue));

            if (max == min) {
                hue = 0.0;
            } else if (max == red && green >= blue) {
                hue = (1.0 / 6.0) * (green - blue) / (max - min);
            } else if (max == red && green < blue) {
                hue = 1.0 + (1.0 / 6.0) * (green - blue) / (max - min);
            } else if (max == green) {
                hue = (1.0 / 3.0) + (1.0 / 6.0) * (blue - red) / (max - min);
            } else if (max == blue) {
                hue = (2.0 / 3.0) + (1.0 / 6.0) * (red - green) / (max - min);
            }

            if (max == 0.0) {
                sat = 0.0;
            } else {
                sat = 1.0 - (min / max);
            }
            val = max;
        }

        public void setHSV(double hue, double sat, double val) {
            double r, g, b;
            double v1, v2;
            hue = hue % 1.0;
            r = 0.0;
            g = 0.0;
            b = 0.0;
            if (sat == 0.0) {
                r = val;
                r = val;
                r = val;
            } else {
                if (val < 0.5) {
                    v2 = val * (1.0 + sat);
                } else {
                    v2 = (val + sat) - (sat * val);
                }
                v1 = 2.0 * val - v2;
                r = hue_to_rgb(v1, v2, hue + (1.0 / 3.0));
                g = hue_to_rgb(v1, v2, hue);
                b = hue_to_rgb(v1, v2, hue - (1.0 / 3.0));

            }
            this.red = r;
            this.green = g;
            this.blue = b;
            this.alpha = 1.0;
            this.hue = hue;
            this.sat = sat;
            this.val = val;

        }

        private double hue_to_rgb(double v1, double v2, double h) {
            double val = 0.0;
            h = h % 1.0;
            if ((6.0 * h) < 1.0) {
                val = v1 + (v2 - v1) * 6.0 * h;
            } else if ((2.0 * h) < 1.0) {
                val = v2;
            } else if ((3.0 * h) < 2.0) {
                val = v1 + (v2 - v1) * 6.0 * (2.0 / 3.0 - h);
            } else {
                val = v1;
            }
            return val;
        }

        @Override
        public String toString() {

            return "{rgba}: " +
                    Double.toString(this.red) + ", " +
                    Double.toString(this.green) + ", " +
                    Double.toString(this.blue) + ", " +
                    Double.toString(this.alpha) +
                    "\n{hsv}: " +
                    Double.toString(this.hue) + ", " +
                    Double.toString(this.sat) + ", " +
                    Double.toString(this.val);

        }
    }
}

