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

public class Pipeline extends Applet implements MouseListener, MouseMotionListener
{
  final static int CHUNKSIZE = 100;
  Raster 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;
  Surface surfaceList[];
  int surfaces;

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

  public void init( )
  {
    // Declarations
    int i;

    raster = new Raster(getSize().width, getSize().height);
    
    addMouseMotionListener(this);
    addMouseListener(this);

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

    cull = true;

    if (getParameter("cull") != null)
      if (getParameter("cull").equals("false"))
	cull = false;

    canonicalList = new Vertex3D[vertList.length];
    worldList = new Vertex3D[vertList.length];

    for (i = 0; i < vertices; i++) 
      {
	canonicalList[i] = new Vertex3D();
	worldList[i] = new Vertex3D();
      }
    
    // Initialize the normals on all the triangles
    triList[0].setVertexList(vertList);
    for(i=0;i<triangles;i++)
      triList[i].initNormal();

    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*getSize().height)/getSize().width;

    // This gives a perspective projection
    view.perspective(-t, t, -s, s, -1, -200);

    // This sets the look-at point
    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);
		  
		  // Add the face to the appropriate Vertex3Ds' face lists
		  vertList[v0].addFace(triangles);
		  vertList[v1].addFace(triangles);
		  vertList[v2].addFace(triangles);

		  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());
      }
  }
  
  public void paint(Graphics g)
  {
    g.drawImage(screen, 0, 0, this);
  }
  
  public void update(Graphics g)
  {
    paint(g);
  }
  
  int oldx, oldy;
  
  public void mousePressed(MouseEvent e)
  {
    if (e.isMetaDown()) 
      {
	System.out.println("Reset view matrix");
	eye = new Point3D(initEye);
	lookat = new Point3D(initLookat);
	view.loadIdentity();
	float t = (float) Math.sin(Math.PI*(fov/2)/180);
	float s = (t*getSize().height)/getSize().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 = e.getX();
    oldy = e.getY();
  }
  
  public void mouseDragged(MouseEvent e)
  {
    if (e.isMetaDown()) 
      {
	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)(e.getX() - oldx)/t));
	r.rotate(up.x, up.y, up.z, t);
	eye.x = eye.x + ax*(oldy - e.getY())/getSize().height;
	eye.y = eye.y + ay*(oldy - e.getY())/getSize().height;
	eye.z = eye.z + az*(oldy - e.getY())/getSize().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*getSize().height)/getSize().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 = e.getX();
	oldy = e.getY();
	DrawObject();
      }
  }
  
  void DrawObject()
  {
    long time = System.currentTimeMillis();
    showStatus("Drawing " + triangles + " triangles ...");
    
    /*
      ... cull and illuminate in world space ...
      */
    model.transform(vertList, worldList, vertices);
    model.updateInverse();
    triList[0].setVertexList(worldList);
    //for (int i = 0; i < triangles; i++)
    //  // transform the normal
    //  triList[i].updateNormal(model);
    
    for (int i = 0; i < triangles; i++)
      triList[i].Illuminate(lightList, lights, eye, cull, model);
    
    /*
      .... clip in canonical eye coordinates ...
      */
    view.transform(worldList, canonicalList, vertices);
    triList[0].setVertexList(canonicalList);
    raster.fill(Color.white);
    raster.resetz();
    for (int i = 0; i < triangles; i++) 
      {
	// check if trivially rejected
	if (triList[i].visible) 
	  {
	    triList[i].ClipAndDraw(raster, project);
	  }
      }
    time = System.currentTimeMillis() - time;
    showStatus("Time = "+time+" ms");
    screen = raster.toImage( );
    repaint();
  }
  
  public void mouseExited(MouseEvent e)
  {
  }
  public void mouseClicked(MouseEvent e)
  {
  }
  public void mouseReleased(MouseEvent e)
  {
  }
  public void mouseEntered(MouseEvent e)
  {
  }

  public void mouseMoved(MouseEvent e)
  {
  }
}
