/*
 * Bouncy.java
 *
 * (C) 2009, Alex S.
 */


/*
 horribly designed applet
 */

import java.awt.*;
import java.util.*;

// round bouncy thing; as well as a release station.
class Thing {
    public double x,y,z;        // location
    public double vx,vy,vz;     // velocity in each axis
    public double ax,ay,az;     // acceleration in each axis

    public Color color = Color.red;         // color.
    public double rate = 0.3;         // release rate (per second)
    public long lastrelease = 0;        // timestamp of last release.

    // a thing is just a history of states (ie: of things).
    public pList hist = new pList();   // history (last in history is current).

    public Thing(double x,double y,double z,
                double vx,double vy,double vz,
                double ax,double ay,double az){
        this.x = x; this.y = y; this.z = z;
        this.vx = vx; this.vy = vy; this.vz = vz;
        this.ax = ax; this.ay = ay; this.az = az;
    }

    public Thing dup(){     // factory method
        return new Thing(x,y,z,vx,vy,vz,ax,ay,az);
    }

    // dupliate Thing but add velocity noise.
    public Thing dupNoise(){
        return new Thing(x,y,z,
            vx+(Math.random()-0.5)*(0.10*vx),
            vy+(Math.random()-0.5)*(0.10*vy),
            vz+(Math.random()-0.5)*(0.10*vz),
            ax,ay,az
        );
    }

    // find next location tim seconds in the future.
    public Thing next(double tim){
        Thing t = dup();
        // change velocity by acceleration per second.
        t.vx += ax * tim;
        t.vy += ay * tim;
        t.vz += az * tim;
        // chagne position by velocity per second.
        t.x += vx * tim;
        t.y += vy * tim;
        t.z += vz * tim;
        return t;
    }
}

/**
 * spaghetti code galore
 */
public class Bouncy extends BufferedApplet {

    pMatrix matrix = pMatrix.I(4,4);    // model transform
    pMatrix pmatrix = pMatrix.I(4,4);   // screen/perspective
    
    // let user rotate thing
    pMatrix ocamera = pMatrix.toQuaternion(0,0,0);

    // so we wouldn't have setup these params everytime.
    Thing thingtemplate = new Thing(
        -100,   // x
        100,    // y
        -50,    // z
        10,      // vx
        0,      // vy
        0,     // vz
        0,      // ax
        -9.8,   // ay (meters per second, down; earth's gravity, or so they say).
        0       // az
    );

    // list of ball shooters
    pList shooters = new pList();

    // list of released items (balls).
    pList things = new pList();
 
    // sim units per meter
    // removed support for this (makes code a bit ugly at this point).
    // double unitspermeter = 1;

    // track passage of time (actions will happen in ``real time'').
    long lastmillis = 0;

    // mouse state
    private int m_mousex,m_mousey;

    /**
     * initialize things
     */
    public void init(){
        super.init();
        m_mousex = m_mousey = 0;

        // setup screen transform.
        pmatrix.translate3dEq(bounds().width/2,bounds().height/2,0);
        pmatrix.perspective3d(bounds().width/2.0,-1);
        pmatrix.reflecty3dEq();     // make sure Y axis points up.

        // setup ball release stations; 
        // 5 release stations: 
        //      equally spaced out at different heights and different colors.
        Thing t;

        t = thingtemplate.dup();
        t.z = -75;    // midpoint.
        t.y = 50;   
        t.rate = 0.1;
        t.color = Color.blue;
        shooters.push(t);

        t = thingtemplate.dup();
        t.z = -43;    // midpoint.
        t.y = 75;   
        t.rate = 0.1;
        t.color = Color.green;
        shooters.push(t);

        t = thingtemplate.dup();
        t.z = 0;    // midpoint.
        t.y = 100;   
        t.rate = 0.1;
        t.color = Color.black;
        shooters.push(t);
        
        t = thingtemplate.dup();
        t.z = 43;    // midpoint.
        t.y = 75;   
        t.rate = 0.1;
        t.color = Color.yellow;
        shooters.push(t);
        
        t = thingtemplate.dup();
        t.z = 75;   // twice as close.
        t.y = 50;   // half the height.
        t.rate = 0.1;
        t.color = Color.red;
        shooters.push(t);
    }

    // draw string (in 3D, yah!)
    public void drawString(Graphics g,String s,double x,double y,double z){
        double[] a = pmatrix.mult(matrix.mult(new double[]{x,y,z,1}));
        a[0] = a[0] / a[3]; a[1] = a[1] / a[3];
        g.drawString(s,(int)a[0],(int)a[1]);
    }
    
    // draw line.
    public void drawLine(Graphics g,double x1,double y1,double z1,double x2,double y2,double z2){
        double[] a = pmatrix.mult(matrix.mult(new double[]{x1,y1,z1,1}));
        a[0] = a[0] / a[3]; a[1] = a[1] / a[3];
        double[] b = pmatrix.mult(matrix.mult(new double[]{x2,y2,z2,1}));
        b[0] = b[0] / b[3]; b[1] = b[1] / b[3];
        g.drawLine((int)a[0],(int)a[1],(int)b[0],(int)b[1]);
    }
    
    // draw sphere.
    public void drawSphere(Graphics g,double x,double y,double z,double r){
        double[] a = pmatrix.mult(matrix.mult(new double[]{x,y,z,1}));
        a[0] = a[0] / a[3]; a[1] = a[1] / a[3];
        g.fillOval((int)a[0],(int)a[1],(int)(r*2),(int)(r*2));
    }

    // draw box.
    public void drawSquare(Graphics g,double x,double y,double z,double r){
        double[] a = pmatrix.mult(matrix.mult(new double[]{x,y,z,1}));
        a[0] = a[0] / a[3]; a[1] = a[1] / a[3];
        g.fillRect((int)a[0],(int)a[1],(int)(r*2),(int)(r*2));
    }

    // grid for the "floor" (ie: XZ plane, with Y pointing up).
    public void drawAxis(Graphics g){
        g.setColor(Color.darkGray);       
        for(int i=-100;i<=100;i+=5){
            drawLine(g,i,0,100,i,0,-100);
            if(i%50==0)
                drawString(g,""+i+"X",i,-10,100);
        }
        for(int i=-100;i<=100;i+=5){
            drawLine(g,100,0,i,-100,0,i);
            if(i%50==0 && i < 100)
                drawString(g,""+i+"Z",100,-10,i);
        }
        drawLine(g,0,0,0,0,100,0);
        drawString(g,"100Y",0,100,0);
    }

    // applies collision detect against a plane.
    // if not detected, returns 1.
    // if detected, returns 0..1 ratio of where between
    // start and end it occurs.
    public double testcollision(Thing s,Thing e,double[] p){
        double d1 = p[0]*s.x + p[1]*s.y + p[2]*s.z + p[3];        
        double d2 = p[0]*e.x + p[1]*e.y + p[2]*e.z + p[3];
        if( (d2 >= 0 && d1 >= 0) || (d2 <= 0 && d1 <= 0))
            return 1;   // no collision.
        return d1 / (d2 - d1);      // collision.
    }

    /**
     * function that gets called whenever something needs to be
     * rendered.
     */
    public void render(Graphics g) {

        // keep track of time.
        if(lastmillis == 0)
            lastmillis = System.currentTimeMillis();

        long currmillis = System.currentTimeMillis();
        double secondspast = (currmillis - lastmillis) / 1000.0;
        lastmillis = currmillis;

        // go through list of shooters, see if they wanna shoot (rate).
        for(int i=0;i<shooters.size();i++){
            Thing t = (Thing)shooters.get(i);
            double past = (currmillis - t.lastrelease) / 1000.0;
            if(past > 10)   // if haven't shot in 10 seconds, reset timer.
                t.lastrelease = (long)(currmillis - (1.0/t.rate * 1000.0));
            if(past > 1.0/t.rate){      // if time to shoot...
                t.lastrelease = (long)(t.lastrelease + (1.0/t.rate * 1000.0));
                Thing a = t.dupNoise();     // create thing with noise (from shooter params).
                a.color = t.color;          // copy shooter's color.
                a.hist.push(a);             // add first position to "thing" history.
                things.push(a);             // add to list of things.
            }
        }

        {   // go thorugh list of things and update position.
            pList out = new pList();
            for(int i=0;i<things.size();i++){
                Thing t = (Thing)things.get(i);
                
                Thing s = (Thing)t.hist.get(-1);        // previous (start) position.
                Thing e = s.next(secondspast);          // next position (time units into future)

                double r = testcollision(s,e,new double[]{0,1,0,0});
                if(r < 1){  // if detected collision... r fraction of time into future.
                    e = s.next(secondspast * r);        // move to point r fraction time into future.
                    e.vy = -e.vy * 0.9;                // reverse Y velocity, and reduce y speed by 10%
                    t.hist.push(e);                     // save point.
                    s = e;                              // this is now our "start"
                    // cointinue on from this new start for the remainder of time fraction.
                    e = s.next(secondspast - (secondspast * r));
                }
                
                // save new position.
                t.hist.push(e);
                // only keep last 100 positions
                // this is what makes the parabola line, the last point is the ball.
                while(t.hist.size() > 100)
                    t.hist.shift();

                // get first point, if first point is out of bounds, remove thing from list.
                // (ie: this is ugly code).
                s = (Thing)t.hist.get(0);
                if(s.x > 105 || s.x < -105 || 
                   s.y > 105 || s.y < -105 || 
                   s.z > 105 || s.z < -105){
                    // drop that thing.
                }else{
                    out.push(t);
                }
            }
            things = out;
        }

        //if (damage || true) {
                
            // clear screen.
            g.setColor(Color.lightGray);
            g.fillRect(0, 0, bounds().width, bounds().height);

            // save matrix for this run.
            matrix.push();

            matrix.translate3dEq(0,0,-200);     // move back a bit.
            //matrix.scale3dEq(2.6);              // make it bigger.

            matrix.multEq(ocamera.quatToM());   // apply user's mouse rotations.

            //matrix.translate3dEq(0,0,0);

            drawAxis(g);        // d0h!
            
            // shooters are square.
            for(int i=0;i<shooters.size();i++){
                Thing t = (Thing)shooters.get(i);
                g.setColor(t.color);
                drawSquare(g,t.x,t.y,t.z,3.5);
            }
            
            // balls are...well...round.
            for(int i=0;i<things.size();i++){
                Thing t = (Thing)things.get(i);
                g.setColor(t.color);
                // for the history, draw lines (ie: this creates the parabola).
                for(int j=1;j<t.hist.size();j++){
                    Thing t1 = (Thing)t.hist.get(j-1);                    
                    Thing t2 = (Thing)t.hist.get(j);
                    drawLine(g,t1.x,t1.y,t1.z,t2.x,t2.y,t2.z);
                }
                // last point becomes the ball.
                Thing t1 = (Thing)t.hist.get(-1);
                drawSphere(g,t1.x,t1.y,t1.z,3.5);
            }

            // restore matrix.
            matrix.pop();
        //}
    }

    /**
     * mouse event handler
     */
    public boolean mouseDown(Event evt, int x, int y){
        m_mousex = x;
        m_mousey = y;
        return true;
    }
    public boolean mouseUp(Event evt, int x, int y){
        return true;
    }

    /**
     * mouse event handler: let the user move things
     */
    public boolean mouseDrag(Event evt, int x, int y){
        ocamera.set(
            pMatrix.toQuaternion(
                0.015 * (y - m_mousey),
                0.015 * (x - m_mousex),
                0
            ).quatMult(ocamera)
        );
        
        m_mousex = x;
        m_mousey = y;

        damage = true;
        return true;
    }
}


