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;
  ZRaster raster;
  Image screen;
  Vertex3D vertList[];
  Vertex3D worldList[];
  Vertex3D canonicalList[];
  int vertices;
  Triangle triList[];
  int triangles;
  Matrix3D view1, view2;
  Matrix3D model;
  Matrix3D project;
  boolean cull=true;

  Light lightList[];
  int lights;
  Surface surfaceList[];
  int surfaces;
  Point3D eye, lookat, up;
  Point3D initEye, initLookat;
  float fov;

  public void init() {

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

    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);
    view1=new Matrix3D();
    view2=new Matrix3D();
    float t=(float) Math.sin(Math.PI*(fov/2)/180);
    float s=(t*size().height)/size().width;
    view1.perspective(-t, t, -s, s, -1, -200);
    view2.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;
  }

  private 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();
	    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] = new Triangle(v0, v1, v2, nx, ny, nz);
	    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());
    }
  }

  public void paint(Graphics g) {

    g.drawImage(screen, 0, 0, this);
  }

  public void update(Graphics g) {

    paint(g);
  }

  int oldx, oldy;
  public boolean mouseDown(Event e, int x, int y) {

    if (e.metaDown()) {
      showStatus("Reset view matrix");
      eye.copy(initEye);
      lookat.copy(initLookat);
      project=new Matrix3D(raster);
      view1.loadIdentity();
      view2.loadIdentity();
      float t=(float) Math.sin(Math.PI*(fov/2)/180);
      float s=(t*size().height)/size().width;
      view1.perspective(-t, t, -s, s, -1, -200);
      view2.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())
      showStatus("Reset 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;
    project=new Matrix3D(raster);
      view1.loadIdentity();
      view2.loadIdentity();
      view1.perspective(-t, t, -s, s, -1, -200);
      view2.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.Ntransform(vertList, worldList, vertices, triList, triangles);
    triList[0].setVertexList(worldList);
    for (int i=0; i < triangles; i++) {
      triList[i].IlluminateAndCull(lightList, lights, eye, cull);
    }
        
    /*
      .... clip in canonical eye coordinates ...
    */
    raster.fill(Color.white);
    raster.resetz();
    view2.transform(worldList, canonicalList, vertices);
    project.compose(view1);
    triList[0].setVertexList(canonicalList);
    for (int i=0; i < triangles; i++) {
      if (triList[i].visible)
	triList[i].ClipAndDraw(raster, project);
    }
    time=System.currentTimeMillis() - time;
    showStatus("Time="+time+" ms");
    screen=raster.toImage( );
    repaint();
  }
}
