/*
 * FlightBox.java
 *
 * (C) 2004-2005, Alex S.
 */

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

/**
 * main class of the whole thing
 */
public class FlightBox extends BufferedApplet {

    pMatrix matrix = new pMatrix();
    pMatrix pmatrix = new pMatrix();

    pMap map; 

    // angle, mouse state
    private double m_anglex,m_angley;
    private int m_mousex,m_mousey;

    // used for java's fillpoly thingie... (and some quick & dirty clipping)
    private int[] x;
    private int[] y;
    private int[] z;

    // clippping ...
    private int[] cx;
    private int[] cy;
    private int[] cz;

    private int[][] screen_edges;

    boolean[] keys;

    // viewers direction/location
    pEntity player;

    double acceleration = 0;
    double friction = 0;
    double force = 0;
    double speed = 0;
    double frametime = 0;
    long lastframe = System.currentTimeMillis();

    /**
     * initialize things
     */
    public void init(){
        super.init();

        //map = pMapGen.generateCubes(7);
        map = pMapGen.generateRoundedCubes(32,7);

        map.buildBSP();

        // pEntity[696.536252532293,3252.2114602859024,-3252.8875039509153; [0.2761343203503068,<0.5567707004151814,-0.7533427659789588,-0.21501372335517446>]]

        player = new pEntity(696.536252532293,3252.2114602859024,-3252.8875039509153,
            new pQuaternion(0.2761343203503068,0.5567707004151814,-0.7533427659789588,-0.21501372335517446));
        //player.x = 1000;
        //player.y = 1000;
        //player.z = 1000;

        keys = new boolean[0x1000];
        for (int i=0;i<keys.length;i++)
            keys[i] = false;

        m_mousex = m_mousey = 0;
        m_anglex = m_angley = 0;

        x = new int[128];
        y = new int[128];
        z = new int[128];
        cx = new int[128];
        cy = new int[128];
        cz = new int[128];

        pmatrix.translate(bounds().width/2.0,bounds().height/2,0);
        pmatrix.perspective(bounds().width,1);

    }

    /**
     * crude motion handling
     */
    public void handleMotion(){

        long currentframe = System.currentTimeMillis();
        frametime = (currentframe - lastframe) / (double)1000.0;
        lastframe = currentframe;

        if (m_anglex != 0 || m_angley != 0) {            
            player.orientation = 
                pQuaternion.fromAxisAngle(1,0,0,m_anglex).
                mult(pQuaternion.fromAxisAngle(0,1,0,m_angley)).
                mult(player.orientation);
            m_anglex = m_angley = 0;
            damage = true;
        }

        
        if (keys[127] || keys['q']) {            // del key
            player.orientation = 
                pQuaternion.fromAxisAngle(0,0,1,-Math.PI/100).
                mult(player.orientation);
            damage = true;
        }

        if (keys[1025] || keys['e']) {            // ins key
            player.orientation = 
                pQuaternion.fromAxisAngle(0,0,1,Math.PI/100).
                mult(player.orientation);
            damage = true;
        }

        if (keys[1006] || keys['a']) {            // left key
            player.orientation = 
                pQuaternion.fromAxisAngle(0,1,0,-Math.PI/100).
                mult(player.orientation);
            damage = true;
        }

        if (keys[1007] || keys['d']) {            // right key
            player.orientation = 
                pQuaternion.fromAxisAngle(0,1,0,Math.PI/100).
                mult(player.orientation);
            damage = true;
        }

        if (keys[1004] || keys['w']) {            // up key
            player.orientation = 
                pQuaternion.fromAxisAngle(1,0,0,-Math.PI/100).
                mult(player.orientation);
            damage = true;
        }

        if (keys[1005] || keys['s']) {            // down key
            player.orientation = 
                pQuaternion.fromAxisAngle(1,0,0,Math.PI/100).
                mult(player.orientation);
            damage = true;
        }

        if (keys['<'] || keys[','] || keys['z']) {
            //System.out.println("< is pressed");
            force = 100;
        } else if (keys['>'] || keys['.'] || keys['x']) {
            force = -70;
        } else {
            force = 0;
        }        
         
        friction = (double)0.1;
        acceleration = acceleration + friction * (0 - acceleration);
        acceleration = Math.min(1000,acceleration + force); // should be force * direction vector
        speed = speed + friction * (0 - speed);
        speed = Math.min(2000,speed + acceleration * frametime);
        speed = Math.max(speed,-1000);
        double distance = Math.round(speed * frametime); 

        pQuaternion tmpq = new pQuaternion(0,0,0,-1);
        //tmpq = player.orientation.mult(tmpq).mult(player.orientation.conjugate());
        tmpq = player.orientation.conjugate().mult(tmpq).mult(player.orientation);
        //System.out.println("<"+tmpq.x+","+tmpq.y+","+tmpq.z+">\n");
        
        if (distance != 0) {
            double[] eye = {player.x,player.y,player.z};
            double[] neweye = {
                player.x + distance * tmpq.x,
                player.y + distance * tmpq.y,
                player.z + distance * tmpq.z
            };
            //System.out.println("("+player.x+","+player.y+","+player.z+") => ");
            for(int i=0;i<5;i++){
                map.traceSphere(eye,neweye,24);
                
                /*
                if(!map.outputStartsOut){
                    System.out.println("Starting location is inside!");
                }
                if(map.outputAllSolid){
                    System.out.println("Starting and ending inside.");
                }
                if(map.outputStartsOut && map.outputFraction < 1){
                    System.out.println("Moving into object.");
                }
                */
                if(map.outputFraction > 0){
                     
                    //double d = map.outputPlane.eval(neweye);
                    //System.out.println("Final position: "+d+" away frm plane.");
                                        
                    player.x += (neweye[0] - player.x) * map.outputFraction; 
                    player.y += (neweye[1] - player.y) * map.outputFraction;
                    player.z += (neweye[2] - player.z) * map.outputFraction;

                    //System.out.println(" -> ("+player.x+","+player.y+","+player.z+") => ");
                    break;
                }
                double d = map.outputPlane.eval(neweye);
                //System.out.println("Moved "+d+" into the plane.");

                //EndPosition += Polygon.Normal*(CollRadius-EndSideValue)
                neweye[0] += map.outputPlane.p[0] * ((24+pMap.EPSILON)-d);
                neweye[1] += map.outputPlane.p[1] * ((24+pMap.EPSILON)-d);
                neweye[2] += map.outputPlane.p[2] * ((24+pMap.EPSILON)-d);

                d = map.outputPlane.eval(neweye);
                ///System.out.println("Handled move; now "+d+" away from the plane.");
            }

            damage = true;
        }

    }

    /**
     * recompute various things when screen size changes
     */
    public void recompute(Rectangle r){
        screen_edges = new int[4][3];
        int W = r.width-1;
        int H = r.height-1;
        int lines[][] = {
            {0,0,0,H},
            {0,H,W,H},
            {W,H,W,0},
            {W,0,0,0}
        };
        int x1 = 0;
        int y1 = 1;
        int x2 = 2;
        int y2 = 3;
        for (int i=0;i<screen_edges.length;i++) {
            int dx = lines[i][x2] - lines[i][x1];
            int dy = lines[i][y2] - lines[i][y1];
            int A = dy;
            int B = -dx;
            int D = -(A * lines[i][x1] + B * lines[i][y1]);
            screen_edges[i][0] = A;
            screen_edges[i][1] = B;
            screen_edges[i][2] = D;
        }

    }

    /**
     * given a face, transform, clip and render
     */
    public void pRenderFace(pFace face,Graphics g){
        pWinding w = face.winding;

        //System.out.println("render face: "+w.size);

        boolean allnegative = true,allpositive = true,alloutofrange = true,allinrange = true;
        int clipz;
        int n = w.size;
        int k;

        //System.out.print("Points: ");

        for (int i=0;i<n; i++) {

            double[] point = {w.x[i],w.y[i],w.z[i],1};
            point = matrix.mult(point);

            x[i] = (int)point[0];
            y[i] = (int)point[1];
            z[i] = clipz = (int)point[2];

            //System.out.print("("+x[i]+","+y[i]+","+z[i]+") ");

            // clipping
            // trivial cases, all points have positive or all negative z
            if (clipz > 0) {
                allnegative = false;
            } else {
                allpositive = false;
                if (clipz > -10000) {
                    alloutofrange = false;
                } else {
                    allinrange = false;
                }
            }
        }
        //System.out.println("");

        // trivial rejection
        if (allpositive) {
            //System.out.println("\tall positive");
            return;            
        }
        if (alloutofrange) {
            //System.out.println("\tall out of range.");
            return;
        }
        if (!allnegative) {
            int[] arr = {0,0,-1,0};
            n = SutherlandHodgmanPolygonClip3D(x,y,z,n,cx,cy,cz,arr);                    
            if (n == 0) {
                //System.out.println("all z clipped");
                return;
            }
            for (k=0;k<n;k++) {
                x[k] = cx[k]; y[k] = cy[k]; z[k] = cz[k];
            }
        }
        if (!allinrange) {
            int[] arr = {0,0,1,10000};
            int m = SutherlandHodgmanPolygonClip3D(x,y,z,n,cx,cy,cz,arr);
            if (m == 0) {
                //System.out.println("all far clipped");
                return;
            }
            if (n != m) {
                n = m;
                for (k=0;k<n;k++) {
                    x[k] = cx[k]; y[k] = cy[k]; z[k] = cz[k];
                }
            }
        }

        // project face coordinates
        for (k=0;k<n;k++) {
            double[] point = {x[k],y[k],z[k],1}; 
            point = pmatrix.mult(point);
            x[k] = (int)(point[0] / point[3]);
            y[k] = (int)(point[1] / point[3]);
        } 
        n = SutherlandHodgmanPolygonClip(x,y,n,cx,cy,screen_edges[0]);
        if (n == 0) {
            //System.out.println("all screen clipped");
            return;
        }
        n = SutherlandHodgmanPolygonClip(cx,cy,n,x,y,screen_edges[1]);
        if (n == 0) {
            //System.out.println("all screen clipped");
            return;
        }
        n = SutherlandHodgmanPolygonClip(x,y,n,cx,cy,screen_edges[2]);
        if (n == 0) {
            //System.out.println("all screen clipped");
            return;
        }
        n = SutherlandHodgmanPolygonClip(cx,cy,n,x,y,screen_edges[3]);
        if (n == 0) {
            //System.out.println("all screen clipped");
            return;
        }
        g.setColor(face.color);
        g.fillPolygon(x,y,n);
        g.setColor(Color.black);
        g.drawPolygon(x,y,n);
    }

    /**
     * given a list of faces, loop, do back-face-culling, and call rendering func
     */
    public void pRenderFaces(Vector faces,Graphics g){
        Enumeration enum = faces.elements();
        while (enum.hasMoreElements()) {
            pFace face = (pFace)enum.nextElement();
            if (face.plane.eval(player.x,player.y,player.z) > 0)
                pRenderFace(face,g);
        }
    }

    /**
     * render everything in a bsp tree using painter's algorithm (back to front)
     */
    public void pTreeRender(pBSPNode node,Graphics g){
        if (node.type != pBSPNode.TREENODE)
            return;

        // just render everything here.

        if (node.plane.eval(player.x,player.y,player.z) >= 0) {
            pTreeRender(node.back,g);
            pRenderFaces(node.faces,g);
            pTreeRender(node.front,g);
        } else {
            pTreeRender(node.front,g);
            pRenderFaces(node.faces,g);
            pTreeRender(node.back,g);
        }
    }

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

        handleMotion();

        if (damage) {
            g.setColor(Color.lightGray);
            g.fillRect(0, 0, bounds().width, bounds().height);

            matrix.push();

            pMatrix camera = new pMatrix(player.orientation.to4x4matrix());
            matrix.mult(camera);
            matrix.translate(-player.x,-player.y,-player.z);

            pTreeRender(map.bsp,g);

            matrix.pop();

            //g.drawString("("+player.x+","+player.y+","+player.z+")",10,10);

            //System.out.println("camera: "+player);

        }
    }

    /**
     * mouse event handler: let the user move things
     */
    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){
        m_angley += (double)0.015 * (double)(x - m_mousex);
        m_anglex += (double)0.015 * (double)(m_mousey - y);
        m_mousex = x;
        m_mousey = y;
        return true;
    }

    public boolean keyDown(Event evt,int key){
        //System.out.println("keyDown: "+key);
        if (key < 0x1000)
            keys[key] = true;
        return true;
    }

    public boolean keyUp(Event evt,int key){
        //System.out.println("keyUp: "+key);
        if (key < 0x1000)
            keys[key] = false;
        return true;
    }

    /**
     * edge is in Ax + By + D = 0 format.
     *
     * dx = x2 - x1;
     * dy = y2 - y1;
     * A = dy
     * B = -dx
     * D = -(A * x1 + B * y1)
     */
    private int SutherlandHodgmanPolygonClip(int[] ix,int[] iy,int n,int[] ox,int[] oy,int[] edge){
        int A,B,D,p,s,k,l,olen,x,y;
        long d1,d2; // these -need- to be long (some of these values get -very- large.
        A = edge[0];
        B = edge[1];
        D = edge[2];
        olen = 0;
        s = n - 1;
        for (p=0;p<n;p++) {
            if ( (k = ix[p] * A + iy[p] * B + D) >= 0 ) {    // p is inside
                if ( (l = ix[s] * A + iy[s] * B + D) >= 0 ) {  // s in inside
                    ox[olen] = ix[p];
                    oy[olen] = iy[p];
                    olen++;
                } else {
                    // find intersection
                    d1 = -l;
                    d2 = (d1 + k);
                    ox[olen] = (int)(ix[s] + ( d1 * ( ix[p] - ix[s] ) ) / d2);
                    oy[olen] = (int)(iy[s] + ( d1 * ( iy[p] - iy[s] ) ) / d2);
                    olen++;
                    ox[olen] = ix[p];
                    oy[olen] = iy[p];
                    olen++;
                }
            } else {
                if ( (l = ix[s] * A + iy[s] * B + D) >= 0 ) {  // s in inside
                    d1 = -k;
                    d2 = (d1 + l);
                    ox[olen] = (int)(ix[p] + ( d1 * ( ix[s] - ix[p] ) ) / d2);
                    oy[olen] = (int)(iy[p] + ( d1 * ( iy[s] - iy[p] ) ) / d2);
                    olen++;
                }
            }
            s = p;
        }
        return olen;
    }

    /**
     * edge is Ax + By + Cz + D = 0
     */
    private int SutherlandHodgmanPolygonClip3D(int[] ix,int[] iy,int[] iz,int n,int[] ox,int[] oy,int[] oz,int[] edge){
        int A,B,C,D,p,s,k,l,olen,x,y;
        long d1,d2; // these -need- to be long (some of these values get -very- large.
        A = edge[0];
        B = edge[1];
        C = edge[2];
        D = edge[3];
        olen = 0;
        s = n - 1;
        for (p=0;p<n;p++) {
            if ( (k = ix[p] * A + iy[p] * B + iz[p] * C + D) >= 0 ) {    // p is inside
                if ( (l = ix[s] * A + iy[s] * B + iz[s] * C + D) >= 0 ) {  // s in inside
                    ox[olen] = ix[p];
                    oy[olen] = iy[p];
                    oz[olen] = iz[p];
                    olen++;
                } else {
                    // find intersection
                    d1 = -l;
                    d2 = (d1 + k);
                    ox[olen] = (int)(ix[s] + ( d1 * ( ix[p] - ix[s] ) ) / d2);
                    oy[olen] = (int)(iy[s] + ( d1 * ( iy[p] - iy[s] ) ) / d2);
                    oz[olen] = (int)(iz[s] + ( d1 * ( iz[p] - iz[s] ) ) / d2);
                    olen++;
                    ox[olen] = ix[p];
                    oy[olen] = iy[p];
                    oz[olen] = iz[p];
                    olen++;
                }
            } else {
                if ( (l = ix[s] * A + iy[s] * B + iz[s] * C + D) >= 0 ) {  // s in inside
                    d1 = -k;
                    d2 = (d1 + l);
                    ox[olen] = (int)(ix[p] + ( d1 * ( ix[s] - ix[p] ) ) / d2);
                    oy[olen] = (int)(iy[p] + ( d1 * ( iy[s] - iy[p] ) ) / d2);
                    oz[olen] = (int)(iz[p] + ( d1 * ( iz[s] - iz[p] ) ) / d2);
                    olen++;
                }
            }
            s = p;
        }
        return olen;
    }
}


