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;

/***************************************************************************/
/* Modified by Amit Khetan                                                 */
/* 11/25/98                                                                */
/***************************************************************************/

public class Pipeline extends Applet {
  final static int CHUNKSIZE = 100;
  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;
  
  Light lightList[];
  int lights;
  int oldlights;
  Surface surfaceList[];
  int surfaces;
  
  Point3D eye, lookat, up;
  Point3D initEye, initLookat;
  float fov;
  
  Label xLabel, yLabel, zLabel;
  Label rVal, bVal, gVal;
  TextField xField, yField, zField, rField, gField, bField;
  
  Button addLight;
  Button removeLight;
  Button rotate;

  Choice lightChoice;
  
  public void init( )
  {
    
    
    
    addLight = new Button("Add Light");
    removeLight = new Button("Remove Light");
    rotate = new Button("Rotate");

    xLabel = new Label("x pos: ");
    yLabel = new Label("y pos: ");
    zLabel = new Label("z pos: ");
    rVal = new Label("red: ");
    bVal = new Label("blue: ");
    gVal = new Label("green: ");
    xField = new TextField("10");
    yField = new TextField("10");
    zField = new TextField("10");
    
    rField = new TextField("1");
    gField = new TextField("1");
    bField = new TextField("1");
    //initialize lightChoice
    lightChoice = new Choice();
    lightChoice.addItem("Directional");
    lightChoice.addItem("Point");
    
    this.add(lightChoice);
    this.add(xLabel);
    this.add(xField);
    this.add(yLabel);
    this.add(yField);
    this.add(zLabel);
    this.add(zField);
    this.add(rVal);
    this.add(rField);
    this.add(gVal);
    this.add(gField);
    this.add(bVal);
    this.add(bField);
    this.add(addLight);
    this.add(removeLight);
    this.add(rotate);

    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);
    }
    

    // Set the vertex normals

    for (int i = 0; i < vertices; i++) {
      vertList[i].averageNormals();
    }
    
    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, -20);
    view.lookAt(eye.x, eye.y, eye.z, lookat.x, lookat.y, lookat.z, up.x, up.y, up.z);
    model = new Matrix3D();
    DrawObject();
  }
  
  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);
	    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);
	    if (faceTris == 0) {
	      // the normal could be computed here instead if all
	      // facets are planar... I'll just play it safe
	      vertList[v0].addNormal(nx, ny, nz);
	      vertList[v1].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]);
	    vertList[v2].addNormal(nx, ny, nz);
	    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());
  }
  oldlights = lights;
  }
  
  public void paint(Graphics g)
  {
    g.drawImage(screen, 0, 0, this);
  }
  
  public void update(Graphics g)
  {
    paint(g);
  }
  
  int oldx, oldy;
  

   public boolean action(Event e, Object arg)
  {
    if (e.target == rotate)
      {
	float ax, ay, az;
	ax = ay = az = 1;
	float l =  1/ (float) Math.sqrt(2);
	float theta = (float) Math.asin(l);
	Matrix3D temp = new Matrix3D();
	temp.rotate(ax,ay,az, theta);
	temp.compose(model);
	model = temp;
	model.transform(vertList, worldList, vertices);
	view.transform(worldList, canonicalList, vertices);
	DrawObject();
	return true;
      }

    if (e.target == removeLight)
      {
	if (lights > oldlights)
	  lights--;
	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, -20);
	view.lookAt(eye.x, eye.y, eye.z, lookat.x, lookat.y, lookat.z, up.x, up.y, up.z);
	DrawObject();
      }
    if (e.target == addLight)
      {
        float xval = (Float.valueOf(xField.getText()).floatValue());
        float yval = (Float.valueOf(yField.getText()).floatValue());
        float zval = (Float.valueOf(zField.getText()).floatValue());
        float rval = (Float.valueOf(rField.getText()).floatValue());
	float gval = (Float.valueOf(gField.getText()).floatValue());
	float bval = (Float.valueOf(bField.getText()).floatValue());
	
        if (lightChoice.toString().equals("Directional"))
          lightList[lights] = new Light(Light.DIRECTIONAL, xval, yval, zval, rval, gval, bval);
        else
          lightList[lights] = new Light(Light.POINT, xval, yval, zval, rval, gval, bval);
        lights++;
	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, -20);
	view.lookAt(eye.x, eye.y, eye.z, lookat.x, lookat.y, lookat.z, up.x, up.y, up.z);
	DrawObject();
      }
    return true;
  }

  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, -20);
      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, -20);
      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;
  }
  
  void DrawObject()
  {
    long time = System.currentTimeMillis();
    showStatus("Drawing "+triangles+" triangles ...");
    
    /*
      ... cull and illuminate in world space ...
      */
    model.transform(vertList, worldList, vertices);
    triList[0].setVertexList(worldList);
    //System.out.println("Illuminating");
    for (int i = 0; i < triangles; i++) {
      triList[i].Illuminate(lightList, lights, eye, cull);
    }
    //for (int i = 0; i < vertices; i++)
    // System.out.println("vertex "+i+ " hasColor = "+worldList[i].hasColor+" "+worldList[i].r+" "+worldList[i].g+" "+worldList[i].b);
  
    
    /*
      .... clip in canonical eye coordinates ...
      */
    view.transform(worldList, canonicalList, vertices);
    triList[0].setVertexList(canonicalList);
    raster.fill(Color.black);
    raster.resetz();
    //for (int i = 0; i < vertices; i++)
    //System.out.println("vertex "+i+ " hasColor = "+canonicalList[i].hasColor+" "+canonicalList[i].r+" "+canonicalList[i].g+" "+canonicalList[i].b);
  
    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( );
    repaint();
  }
  
}
