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

public class Pipeline extends Applet {
    final static int CHUNKSIZE = 100;
    ZRaster raster;
    Image screen;
    VertexList vertList;
    VertexList worldList;
    VertexList canonicalList;
    VertexList viewportList;
    //    int vertices;
    Facet faceList[];
    int facets;
    Matrix3D view;
    Matrix3D model;
    Matrix3D project;
    boolean cull = true;
    boolean flat = false;
    
    Light lightList[];
    int lights;
    Surface surfaceList[];
    int surfaces;
    
    Point3D lookat, up;
    Point3D eye, 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 Vertex3D(0, 0, -10);
        lookat = new Point3D(0, 0, 0);
        up = new Point3D(0, 1, 0);
        fov = 30;
	
        vertList = new VertexList();
        faceList = new Facet[CHUNKSIZE];
        facets = 0;
        lightList = new Light[CHUNKSIZE];
        lights = 0;
        surfaceList = new Surface[CHUNKSIZE];
        surfaces = 0;
	Facet.setVertexList(vertList);
	
        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;

	//Added support for flat or per-vertx shading
        if (getParameter("flat") != null)
            if (getParameter("flat").equals("true"))
                flat = true;

	for (int i = 0; i<facets; i++)     //get the facet centroid and normal.
	    faceList[i].CalcCentroid();   //(which gives each vertex a normal too)
	vertList.normalize_normals();     // normalize vertex normals.
	
        viewportList = new VertexList(vertList);
        canonicalList = new VertexList(vertList);
        worldList = new VertexList(vertList);
        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();
    }
    
    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()
    {
        Facet newList[] = new Facet[faceList.length+CHUNKSIZE];
        System.arraycopy(faceList, 0, newList, 0, faceList.length);
        faceList = 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);
		    vertList.append(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 (facets == faceList.length) growList();
			    faceList[facets] = new Facet(v0, v1, v2);
			    if (surfaces == 0) {
				surfaceList[surfaces] = new Surface(0.5f, 0.5f, 0.5f, 1.0f, 1.0f, 1.0f, 5.0f);
				surfaces += 1;
			    }
			    faceList[facets].setSurface(surfaceList[surfaces-1]);
			    v1 = v2;
			    faceTris += 1;
			    facets += 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 ALight(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 DLight(r, g, b, x, y, z);
						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 PLight(r, g, b, x, y, z);
						    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 (facets % 100 == 0) showStatus("facets = "+facets);
	}
        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()) {
            System.out.println("Reset view matrix");
            eye.copy(initEye);
            lookat.copy(initLookat);
	    view.loadIdentity();
	    //view.loadViewPort(raster);
            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) (size().width / (2*Math.tan(((fov * Math.PI)/180)/2)));
	    //float t = (float) (raster.getWidth() / (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.loadViewPort(raster);
	    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;
    }
    
    void DrawObject()
    {
        long time = System.currentTimeMillis();
        showStatus("Drawing "+facets+" facets ...");
        
        /*
            ... cull and illuminate in world space ...
        */
        model.transform(vertList, worldList);
	//model.transform(new Point3D(eye), eye);
	Facet.setVertexList(worldList);
        for (int i = 0; i < facets; i++) {
            faceList[i].Illuminate(lightList, lights, eye, cull, flat);
        }
	
        /*
	  .... clip in canonical eye coordinates ...
        */
        view.transform(worldList, canonicalList);
	project.transform(canonicalList, viewportList);
        Facet.setVertexList(viewportList);
	raster.fill(Color.white);
        //raster.fill(Color.gray);
        raster.resetz();
	raster.setVertexList(viewportList);
        for (int i = 0; i < facets; i++) {
	    //Clip the Facet to the near and far plane.
	    //subdivide into triangles, and rasterize.
	    //Must be done in one step, so we don't permanently deform the 
	    //facet shape in clipping it.
	    faceList[i].ClipAndDraw(raster);
        }
        time = System.currentTimeMillis() - time;
        showStatus("Time = "+time+" ms");
        screen = raster.toImage( );
        repaint();
    }

}
