import java.awt.event.*;

import java.applet.*;
import java.awt.*;
import java.io.*;
import java.net.*;
import Vertex3D;
import Point3D;
import Triangle;
import Matrix3D;
import Light;
import Surface;
import ZRaster;

public class Pipeline extends Applet {
    final static int CHUNKSIZE = 100;
    final static int MAX_TO_DRAW = 10;
    ZRaster raster;
    Image screen;
    Vertex3D vertList[];
    Vertex3D worldList[];
    Vertex3D canonicalList[];
    int vertices;
    Triangle triList[];
    int triangles;
    Matrix3D view;
    Matrix3D model;
    Matrix3D project;
    boolean cull = true;
    // Next four added
    TextField tf1, tf2, tf3;
    Panel  buttons;
    pr1 disp;
    int xSize, ySize;

    Light lightList[];
    int lights;
    Surface surfaceList[];
    int surfaces;

    Point3D eye, lookat, up;
    Point3D initEye, initLookat;
    float fov;

    public void init( )
    {
      // Set the layout manager
      GridBagLayout gridbag = new GridBagLayout();
      GridBagConstraints c = new GridBagConstraints();
      setLayout(gridbag);

      c.weightx      = 1;
      c.weighty      = 1;
      c.gridwidth    = GridBagConstraints.REMAINDER;
      c.anchor       = GridBagConstraints.NORTHWEST;
      c.fill         = GridBagConstraints.BOTH;
      c.ipady       += 2;   // put some extra padding between cells
      disp = new pr1(this);
      gridbag.setConstraints(disp,c);
      add(disp);

      c.weighty      = 0;
      c.anchor       = GridBagConstraints.SOUTHWEST;
      c.fill         = GridBagConstraints.HORIZONTAL;
      buttons = buttonPanel();
      gridbag.setConstraints(buttons,c);
      add(buttons);

xSize = disp.size().width;
ySize = disp.size().height;
if(xSize*ySize == 0) {
   xSize = 400; ySize = 400;
}

//System.out.println("Disp size" + xSize + "  " + ySize);
        raster = new ZRaster(size().width, size().height);

        // initialize viewing parameters to default
        // values in case they are not defined by the
        // input file
        eye = new Point3D(0, 0, -10);
        lookat = new Point3D(0, 0, 0);
        up = new Point3D(0, 1, 0);
        fov = 30;

        vertList = new Vertex3D[CHUNKSIZE];
        vertices = 0;
        triList = new Triangle[CHUNKSIZE];
        triangles = 0;
        lightList = new Light[CHUNKSIZE];
        lights = 0;
        surfaceList = new Surface[CHUNKSIZE];
        surfaces = 0;

        String filename = getParameter("datafile");
        showStatus("Reading "+filename);
        InputStream is = null;
        try {
            is = new URL(getDocumentBase(), filename).openStream();
            ReadInput(is);
            is.close();
        } catch (IOException e) {
            showStatus("Error reading "+filename);
        }
        
        if (getParameter("cull") != null)
            if (getParameter("cull").equals("false"))
                cull = false;
            
        canonicalList = new Vertex3D[vertList.length];
        worldList = new Vertex3D[vertList.length];
        for (int i = 0; i < vertices; i++) {
            canonicalList[i] = new Vertex3D();
            worldList[i] = new Vertex3D();
        }
        initEye = new Point3D(eye);
        initLookat = new Point3D(lookat);
        
        project = new Matrix3D(raster);
        view = new Matrix3D();
        float t = (float) Math.sin(Math.PI*(fov/2)/180);
        float s = (t*size().height)/size().width;
        view.perspective(-t, t, -s, s, -1, -200);
        view.lookAt(eye.x, eye.y, eye.z, lookat.x, lookat.y, lookat.z, up.x, up.y, up.z);
        model = new Matrix3D();
        DrawObject();
    }

    /*
    final static int CHUNKSIZE = 100;
    ZRaster raster;
    Image screen;
    Vertex3D vertList[];
    Vertex3D worldList[];
    Vertex3D tranList[];
    int vertices;
    Matrix3D view;
    Matrix3D model;
    int MAX_TO_DRAW = 10;

    float eyex, eyey, eyez;
    float lookatx, lookaty, lookatz;
    float upx, upy, upz;
    float fov;

    */

class pr1 extends Canvas {
   Pipeline parent;
   public pr1(Pipeline p)
   {
      parent = p;
   }

    public void paint(Graphics g)
    {
//System.out.println("One paint");
//if (screen == null) System.out.println("NULL IMAGE");
        g.drawImage(screen, 0, 0, this);
    }

    public void update(Graphics g)
    {
//System.out.println("One update");
        paint(g);
    }

    float v0x, v0y, v0z;

    /**** OLD STUFF ***
    public boolean mouseDown(Event e, int x, int y)
    {
        if (e.metaDown()) {
            showStatus("Resetting model matrix");
            model.loadIdentity();
            model.transform(vertList, worldList,  vertices);
            view.transform(worldList, tranList, vertices);
            DrawObject();
	   screen = raster.toImage();
	   repaint();
	   return true;
        } else {
	    showStatus("");
	}
	// Compute a vector based on the x y position and normalize it
        v0x = (float) (x - (xSize / 2));
        v0y = (float) ((ySize / 2) - y);
        v0z = (float) xSize;
        float l0 = (float) (1 / Math.sqrt(v0x*v0x + v0y*v0y + v0z*v0z));
        v0x *= l0;
        v0y *= l0;
        v0z *= l0;
        return true;
    }

    public boolean mouseDrag(Event e, int x, int y)
    {
        if (e.metaDown()) {
            showStatus("Resetting model matrix");
        } else {
	    showStatus("");
            float v1x = (float) (x - (xSize / 2));
            float v1y = (float) ((ySize / 2) - y);
            float v1z = (float) xSize;
            float l = (float) (1 / Math.sqrt(v1x*v1x + v1y*v1y + v1z*v1z));
            v1x *= l;
            v1y *= l;
            v1z *= l;

            float ax = v0y*v1z - v0z*v1y;
            float ay = v0z*v1x - v0x*v1z;
            float az = v0x*v1y - v0y*v1x;
            l = (float) Math.sqrt(ax*ax + ay*ay + az*az);
            float theta = (float) Math.asin(l);
            if (v0x*v1x + v0y*v1y + v0z*v1z < 0)
                theta += (float) Math.PI / 2;
	    //System.out.println("Model before" + model.toString());
	    //System.out.println("[[[" + ax + " " + ay + " " + az + " " + theta);
            model.rotate(ax, ay, az, theta);
	    //System.out.println("Model before" + model.toString());
            v0x = v1x;
            v0y = v1y;
            v0z = v1z;
            model.transform(vertList, worldList,  vertices);
            view.transform(worldList, tranList, vertices);
            DrawObject();
        }
        //screen = raster.toImage(this);
        screen = raster.toImage();
        repaint();
        return true;
    }

    *** OLD STUFF ****/

    int oldx, oldy;
    public boolean mouseDown(Event e, int x, int y)
    {
        if (e.metaDown()) {
            showStatus("Resetting model matrix");
            eye.copy(initEye);
            lookat.copy(initLookat);
            view.loadIdentity();
            float t = (float) Math.sin(Math.PI*(fov/2)/180);
            float s = (t*size().height)/size().width;
            view.perspective(-t, t, -s, s, -1, -200);
            view.lookAt(eye.x, eye.y, eye.z, lookat.x, lookat.y, lookat.z, up.x, up.y, up.z);
            DrawObject();
        } else {
	    showStatus("");
        }
        oldx = x;
        oldy = y;
	return true;
    }

    public boolean mouseDrag(Event e, int x, int y)
    {
        if (e.metaDown()) {
            showStatus("Resetting model matrix");
        } else {
	    showStatus("");
            float ax = lookat.x - eye.x;
            float ay = lookat.y - eye.y;
            float az = lookat.z - eye.z;
            float t = (float) (getSize().width / (2*Math.tan(((fov * Math.PI)/180)/2)));
            Matrix3D r = new Matrix3D( );
            t = (float) (Math.atan((double)(x - oldx)/t));
            r.rotate(up.x, up.y, up.z, t);
            eye.x = eye.x + ax*(oldy - y)/size().height;
            eye.y = eye.y + ay*(oldy - y)/size().height;
            eye.z = eye.z + az*(oldy - y)/size().height;
            lookat.x = r.get(0,0)*ax + r.get(1,0)*ay + r.get(2,0)*az;
            lookat.y = r.get(0,1)*ax + r.get(1,1)*ay + r.get(2,1)*az;
            lookat.z = r.get(0,2)*ax + r.get(1,2)*ay + r.get(2,2)*az;
            lookat.x += eye.x;
            lookat.y += eye.y;
            lookat.z += eye.z;
            t = (float) Math.sin(Math.PI*(fov/2)/180);
            float s = (t*size().height)/size().width;
            view.loadIdentity();
            view.perspective(-t, t, -s, s, -1, -200);
            view.lookAt(eye.x, eye.y, eye.z, lookat.x, lookat.y, lookat.z, up.x, up.y, up.z);
            oldx = x;
            oldy = y;
            DrawObject();
        }
        return true;
    }

    /******* NEW STUFF 
    int oldx, oldy;

    public boolean mouseDown(Event e, int x, int y)
    {
        if (e.metaDown()) {
            System.out.println("Reset view matrix");
            eye.copy(initEye);
            lookat.copy(initLookat);
            view.loadIdentity();
            float t = (float) Math.sin(Math.PI*(fov/2)/180);
            float s = (t*size().height)/size().width;
            view.perspective(-t, t, -s, s, -1, -200);
            view.lookAt(eye.x, eye.y, eye.z, lookat.x, lookat.y, lookat.z, up.x, up.y, up.z);
            DrawObject();
        }
        oldx = x;
        oldy = y;
	    return true;
    }

    public boolean mouseDrag(Event e, int x, int y)
    {
        if (e.metaDown()) {
            System.out.println("Resetting view matrix");
        } else {
            float ax = lookat.x - eye.x;
            float ay = lookat.y - eye.y;
            float az = lookat.z - eye.z;
            float t = (float) (getSize().width / (2*Math.tan(((fov * Math.PI)/180)/2)));
            Matrix3D r = new Matrix3D( );
            t = (float) (Math.atan((double)(x - oldx)/t));
            r.rotate(up.x, up.y, up.z, t);
            eye.x = eye.x + ax*(oldy - y)/size().height;
            eye.y = eye.y + ay*(oldy - y)/size().height;
            eye.z = eye.z + az*(oldy - y)/size().height;
            lookat.x = r.get(0,0)*ax + r.get(1,0)*ay + r.get(2,0)*az;
            lookat.y = r.get(0,1)*ax + r.get(1,1)*ay + r.get(2,1)*az;
            lookat.z = r.get(0,2)*ax + r.get(1,2)*ay + r.get(2,2)*az;
            lookat.x += eye.x;
            lookat.y += eye.y;
            lookat.z += eye.z;
            t = (float) Math.sin(Math.PI*(fov/2)/180);
            float s = (t*size().height)/size().width;
            view.loadIdentity();
            view.perspective(-t, t, -s, s, -1, -200);
            view.lookAt(eye.x, eye.y, eye.z, lookat.x, lookat.y, lookat.z, up.x, up.y, up.z);
            oldx = x;
            oldy = y;
            DrawObject();
        }
        return true;
    }
   ******* NEW STUFF */

}

    Choice chooser;
Label dispLabel;
    private Panel buttonPanel()
    {
      // Set the layout manager for the last two buttons on screen
      GridBagLayout gridbag = new GridBagLayout();
      Panel buttons = new Panel(gridbag);
      GridBagConstraints c = new GridBagConstraints();

    chooser = new Choice();
    chooser.add("Translate");
    chooser.add("Scale");
    chooser.add("Skew");
    chooser.add("Identity");
    chooser.add("Orthographic");
    chooser.add("Perspective");
    // get the whole x-width
    c.weightx = 1;
    c.weighty = 1;
    c.gridwidth = 1;
    c.fill = GridBagConstraints.HORIZONTAL;
    // anchor the choice to the left
    c.anchor    = GridBagConstraints.WEST;
    gridbag.setConstraints(chooser,c);
    buttons.add(chooser);

      c.anchor    = GridBagConstraints.WEST;
    c.gridwidth = GridBagConstraints.RELATIVE;
       dispLabel   = new Label("Perspective");
      gridbag.setConstraints(dispLabel,c);
      buttons.add(dispLabel);

      Button timeButton = new Button("Transform");
      timeButton.setActionCommand("Transform");
      timeButton.addActionListener(new ActionListener() 
         {
            public void actionPerformed(ActionEvent e) {
               String s = chooser.getSelectedItem();
/***********************
 if(s.equals("Orthographic")) {
	view = new Matrix3D(raster);
        float t = (float) Math.sin(Math.PI*(fov/2)/180);
        float p = (t*ySize)/xSize;
        view.orthographic(-t*8, t*8, -p*8, p*8, -1, -200);
        view.lookAt(eyex, eyey, eyez, lookatx, lookaty, lookatz, upx, upy, upz);
	    dispLabel.setText("Orthographic");
	    // Show the changes
            model.transform(vertList, worldList,  vertices);
            view.transform(worldList, tranList, vertices);
            DrawObject();
	   screen = raster.toImage();
	   disp.repaint();
return;
	    } else if(s.equals("Identity")) {
			model.loadIdentity();
	    // Show the changes
            model.transform(vertList, worldList,  vertices);
            view.transform(worldList, tranList,  vertices);
            DrawObject();
	   screen = raster.toImage();
	   disp.repaint();
return;
} else if(s.equals("Perspective")) {
	view = new Matrix3D(raster);
        float t = (float) Math.sin(Math.PI*(fov/2)/180);
        float p = (t*ySize)/xSize;
        view.perspective(-t, t, -p, p, -1, -200);
        view.lookAt(eyex, eyey, eyez, lookatx, lookaty, lookatz, upx, upy, upz);
	    dispLabel.setText("Perspective");
	    // Show the changes
            model.transform(vertList, worldList,  vertices);
            view.transform(worldList, tranList,  vertices);
            DrawObject();
	   screen = raster.toImage();
	   disp.repaint();
return;
}
	       float inX, inY, inZ;
	       try {
		  inX = Float.valueOf(tf1.getText()).floatValue();
		  inY = Float.valueOf(tf2.getText()).floatValue();
		  inZ = Float.valueOf(tf3.getText()).floatValue();
	       } catch (NumberFormatException f) {
                  showStatus("Error in getting X, Y, or Z values");
		  return;
	       }
	    if(s.equals("Translate")) {
	       model.translate(inX, inY, inZ);
	    } else if(s.equals("Scale")) {
	       if(inX <= 0 || inY <= 0 || inZ <= 0) {
		  showStatus("Error -- Scale should be positive ONLY!");
		  return;
	       }
	       model.scale(inX, inY, inZ);
	    } else if(s.equals("Skew")) {
	       model.skew(inX, inY, inZ);
	    } else {
***********************/
	       System.err.println("INVALID CHOICE " + s);
/***********************
	    }
	    // Show the changes
            model.transform(vertList, worldList,  vertices);
            view.transform(worldList, tranList, vertices);
***********************/
            DrawObject();
	   screen = raster.toImage();
	   disp.repaint();

            } // end of ActionEvent
         });
    c.gridwidth = GridBagConstraints.REMAINDER;
    c.fill = GridBagConstraints.HORIZONTAL;
    // anchor the choice to the left
    c.anchor    = GridBagConstraints.WEST;
    gridbag.setConstraints(timeButton,c);
    buttons.add(timeButton);
 
    Panel tmpP = getLabels();
    gridbag.setConstraints(tmpP,c);
    buttons.add(tmpP);
    tmpP = getFields();
    gridbag.setConstraints(tmpP,c);
    buttons.add(tmpP);

      return buttons;
    }

   private Panel getLabels()
   {
      // Set the layout manager for the last two buttons on screen
      GridBagLayout gridbag = new GridBagLayout();
      Panel tP = new Panel(gridbag);
      GridBagConstraints c = new GridBagConstraints();

       c.weightx = 1;
       c.weighty = 1;
       c.gridwidth = 1;
      c.anchor    = GridBagConstraints.WEST;
      Label lbl   = new Label("X Input");
      gridbag.setConstraints(lbl,c);
      tP.add(lbl);
      c.gridwidth = GridBagConstraints.RELATIVE;
      lbl   = new Label("Y Input");
      gridbag.setConstraints(lbl,c);
      tP.add(lbl);
      c.gridwidth = GridBagConstraints.REMAINDER;
      lbl   = new Label("Z Input");
      gridbag.setConstraints(lbl,c);
      tP.add(lbl);
      return tP;
   }

    private Panel getFields()
    {
      // Set the layout manager for the last two buttons on screen
      GridBagLayout gridbag = new GridBagLayout();
      Panel tP = new Panel(gridbag);
      GridBagConstraints c = new GridBagConstraints();

       c.weightx = 1;
       c.weighty = 1;
       c.gridwidth = 1;
       tf1 = new TextField(20);
       tf1.setEditable(true);
       gridbag.setConstraints(tf1,c);
       tP.add(tf1);
       c.gridwidth = GridBagConstraints.RELATIVE;
       tf2 = new TextField(20);
       tf2.setEditable(true);
       gridbag.setConstraints(tf2,c);
       tP.add(tf2);
       c.gridwidth = GridBagConstraints.REMAINDER;
       tf3 = new TextField(20);
       tf3.setEditable(true);
       gridbag.setConstraints(tf3,c);
       tP.add(tf3);
       return tP;
    }

    public String toString()
    {
       String out = new String("");

       for (int i = 0; i < vertices; i++) {
	  if(i > MAX_TO_DRAW) {
	     break;
	  } else {
	     out += "Vertex " + i + "\n" +
		    "VertList " + vertList[i].toString() + "\n" +
		    "WorldList" + worldList[i].toString() + "\n";

	  }
       }
       return out;
    }

    private double getNumber(StreamTokenizer st) throws IOException
    {
        if (st.nextToken() != StreamTokenizer.TT_NUMBER) {
            System.err.println("ERROR: line "+st.lineno()+": expected number");
            throw new IOException(st.toString());
        }
        return st.nval;
    }

    private void growList()
    {
        Triangle newList[] = new Triangle[triList.length+CHUNKSIZE];
        System.arraycopy(triList, 0, newList, 0, triList.length);
        triList = newList;
    }

    public void ReadInput(InputStream is) throws IOException
    {
	    StreamTokenizer st = new StreamTokenizer(is);
    	st.commentChar('#');
        scan: while (true) {
	        switch (st.nextToken()) {
	          default:
		        break scan;
	          case StreamTokenizer.TT_WORD:
		        if (st.sval.equals("v")) {
		            float x = (float) getNumber(st);
		            float y = (float) getNumber(st);
		            float z = (float) getNumber(st);
		            if (vertices == vertList.length) {
		                Vertex3D newList[] = new Vertex3D[vertList.length+CHUNKSIZE];
                        System.arraycopy(vertList, 0, newList, 0, vertList.length);
		                vertList = newList;
		            }
		            vertList[vertices++] = new Vertex3D(x, y, z);
		        } else
		        if (st.sval.equals("f")) {
		            int faceTris = 0;
                    int v0 = (int) getNumber(st);
		            int v1 = (int) getNumber(st);
		            while (st.nextToken() == StreamTokenizer.TT_NUMBER) {
		                st.pushBack();
		                int v2 = (int) getNumber(st);
		                if (v2 == v0) continue;
		                if (triangles == triList.length) growList();
		                triList[triangles] = new Triangle(v0, v1, v2);
// For every triangle face in the polygon we are putting into the table
// We calculate the normal and assign it to the different vertices
// Every subsequent triangle of this same face has the same normal!
float nx = (vertList[v1].z - vertList[v0].z) * (vertList[v2].y - vertList[v1].y)
	 - (vertList[v1].y - vertList[v0].y) * (vertList[v2].z - vertList[v1].z);
float ny = (vertList[v1].x - vertList[v0].x) * (vertList[v2].z - vertList[v1].z)
	 - (vertList[v1].z - vertList[v0].z) * (vertList[v2].x - vertList[v1].x);
float nz = (vertList[v1].y - vertList[v0].y) * (vertList[v2].x - vertList[v1].x)
	 - (vertList[v1].x - vertList[v0].x) * (vertList[v2].y - vertList[v1].y);
triList[triangles].setNormal(nx,ny,nz);
/*
vertList[v0].addFace(triList[triangles]);
vertList[v1].addFace(triList[triangles]);
vertList[v2].addFace(triList[triangles]);
vertList[v0].addNormal(nx, ny, nz);
vertList[v1].addNormal(nx, ny, nz);
vertList[v2].addNormal(nx, ny, nz);
*/
                        if (surfaces == 0) {
                            surfaceList[surfaces] = new Surface(0.5f, 0.5f, 0.5f, 1.0f, 1.0f, 1.0f, 5.0f);
		                    surfaces += 1;
                        }
                        triList[triangles].setSurface(surfaceList[surfaces-1]);
		                v1 = v2;
		                faceTris += 1;
                        triangles += 1;
		            }
		            st.pushBack();
			    } else
			    if (st.sval.equals("eye")) {
                    eye.x = (float) getNumber(st);
                    eye.y = (float) getNumber(st);
		            eye.z = (float) getNumber(st);
			    } else
			    if (st.sval.equals("look")) {
                    lookat.x = (float) getNumber(st);
                    lookat.y = (float) getNumber(st);
		            lookat.z = (float) getNumber(st);
			    } else
			    if (st.sval.equals("up")) {
                    up.x = (float) getNumber(st);
                    up.y = (float) getNumber(st);
		            up.z = (float) getNumber(st);
			    } else
			    if (st.sval.equals("fov")) {
                    fov = (float) getNumber(st);
			    } else
			    if (st.sval.equals("la")) {             // ambient light source
                    float r = (float) getNumber(st);
                    float g = (float) getNumber(st);
		            float b = (float) getNumber(st);
		            lightList[lights] = new Light(Light.AMBIENT, 0, 0, 0, r, g, b);
		            lights += 1;
			    } else
			    if (st.sval.equals("ld")) {             // directional light source
                    float r = (float) getNumber(st);
                    float g = (float) getNumber(st);
		            float b = (float) getNumber(st);
		            float x = (float) getNumber(st);
		            float y = (float) getNumber(st);
		            float z = (float) getNumber(st);
		            lightList[lights] = new Light(Light.DIRECTIONAL, x, y, z, r, g, b);
		            lights += 1;
			    } else
			    if (st.sval.equals("lp")) {             // point light source
                    float r = (float) getNumber(st);
                    float g = (float) getNumber(st);
		            float b = (float) getNumber(st);
		            float x = (float) getNumber(st);
		            float y = (float) getNumber(st);
		            float z = (float) getNumber(st);
		            lightList[lights] = new Light(Light.POINT, x, y, z, r, g, b);
		            lights += 1;
			    } else
			    if (st.sval.equals("surf")) {
                    float r = (float) getNumber(st);
                    float g = (float) getNumber(st);
		            float b = (float) getNumber(st);
		            float ka = (float) getNumber(st);
		            float kd = (float) getNumber(st);
		            float ks = (float) getNumber(st);
		            float ns = (float) getNumber(st);
		            surfaceList[surfaces] = new Surface(r, g, b, ka, kd, ks, ns);
		            surfaces += 1;
			    } else {
                    System.err.println("ERROR: line "+st.lineno()+": unexpected token :"+st.sval);
                    break scan;
			    }
			    break;
	        }
	        if (triangles % 100 == 0) showStatus("triangles = "+triangles);
	    }
        is.close();
	    if (st.ttype != StreamTokenizer.TT_EOF) {
	        throw new IOException(st.toString());
	    }
	}


    void DrawObject()
    {
        long time = System.currentTimeMillis();
        showStatus("Drawing "+triangles+" triangles ...");
        
        /*
            ... cull and illuminate in world space ...
        */
        triList[0].setVertexList(worldList);

        model.transform(vertList, worldList, vertices);
        model.transform(triList, true);

        for (int i = 0; i < triangles; i++) {
            triList[i].Illuminate(lightList, lights, eye, cull);
        }
        
        /*
            .... clip in canonical eye coordinates ...
        */
        view.transform(worldList, canonicalList, vertices);
	view.transform(triList, false);
        triList[0].setVertexList(canonicalList);
        raster.fill(Color.white);
        raster.resetz();
        for (int i = 0; i < triangles; i++) {
            // check if trivially rejected
            if (triList[i].isVisible()) {
                triList[i].ClipAndDraw(raster, project);
            }
        }
        time = System.currentTimeMillis() - time;
        showStatus("Time = "+time+" ms");
        screen = raster.toImage( );
        disp.repaint();
    }

}
