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 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;

	// 1: Dots
	// 2: Flat shading
	// 3: Gouraud shading
	int shadestate;
	final static int DOTS = 1;
	final static int FLAT = 2;
	final static int GOURAUD = 3;
	Color shadestatecolor = Color.blue;

	// 1: Perspective
	// 2: Orthographic
	int viewstate;
	final static int PERSPECTIVE = 1;
	final static int ORTHOGRAPHIC = 2;
	Color viewstatecolor = Color.blue;

	// 1: Rotation
	// 2: Translation
	// 3: Scale
	// 4: Skew
	int state;
	final static int NUMSTATE = 4;
	final static int ROTATION = 1;
	final static int TRANSLATION = 2;
	final static int SCALE = 3;
	final static int SKEW = 4;
	Color statecolor = Color.blue;

	Color cullstatecolor = Color.blue;

    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();        
		}

        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();

		// Initial shading is gouraud
		shadestate = GOURAUD;
		// Initially cull
		cull = true;
		// Initial state is rotation
		state = ROTATION;
		// Initial view state is set by changeView() ==> perspective
		viewstate = 0;
		changeView();

		model = new Matrix3D();

		display();
    }

	// change from perspective to orthographic
	//	or change from orthographic to perspective
	private void changeView()
	{
        float t = (float) Math.sin(Math.PI*(fov/2)/180);
        float s = (t*size().height)/size().width;
        view = new Matrix3D();
		if (viewstate == PERSPECTIVE)
		{
			viewstate = ORTHOGRAPHIC;
			view.orthographic(-t*10, t*10, -s*10, s*10, -1*10, -200*10);	// I multiplied everything by 10 to make it look right.
		}
		else
		{
			viewstate = PERSPECTIVE;
			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);
	}

	// Transforms the vertices and draws the object
	private void display()
	{
        DrawObject();
        screen = raster.toImage();
        repaint();
	}

    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());
		}
	}

	public void paint(Graphics g)
	{
		// draws the image
        g.drawImage(screen, 0, 0, this);

		// draws the state on the top-left corner using the state color
		String str = new String();
		switch(state)
		{
		case ROTATION:
			str = new String("Rotation");
			break;
		case TRANSLATION:
			str = new String("Translation");
			break;
		case SCALE:
			str = new String("Scale");
			break;
		case SKEW:
			str = new String("Skew");
			break;
		}
		g.setColor(statecolor);
		g.drawString(str, 10, 20);
		
		// draws the view state on the top-right corner using the viewstate color
		switch(viewstate)
		{
		case PERSPECTIVE:
			str = new String("Perspective");
			break;
		case ORTHOGRAPHIC:
			str = new String("Orthographic");
			break;
		}
		g.setColor(viewstatecolor);
		g.drawString(str, 230, 20);

		// draws the shade state on the bottom-left corner using the shadestate color
		switch(shadestate)
		{
		case DOTS:
			str = new String("Dots");
			break;
		case FLAT:
			str = new String("Flat shading");
			break;
		case GOURAUD:
			str = new String("Gouraud shading");
			break;
		}
		g.setColor(shadestatecolor);
		g.drawString(str, 10, 290);

		// draws the cull state on the bottom-right corner using the cullstate color
		if (cull)
		{
			str = new String("Culling");
		}
		else
		{
			str = new String("No culling");
		}
		g.setColor(cullstatecolor);
		g.drawString(str, 230, 290);

	}

    public void update(Graphics g)
    {
        paint(g);
    }

    public boolean mouseMove(Event e, int x, int y)
    {
		// if mouse is in the top-left corner, than change the state color to red, else blue
		Color oldcolor = statecolor;
		if ((x<60)&&(y<30))
			statecolor = Color.red;
		else
			statecolor = Color.blue;
		if (oldcolor != statecolor)
			repaint();

		// if mouse is in the top-right corner, than change the viewstate color to red, else blue
		oldcolor = viewstatecolor;
		if ((x>size().width-90)&&(y<30))
			viewstatecolor = Color.red;
		else
			viewstatecolor = Color.blue;
		if (oldcolor != viewstatecolor)
			repaint();

		// if mouse is in the bottom-left corner, than change the shade state color to red, else blue
		oldcolor = shadestatecolor;
		if ((x<100)&&(y>270))
			shadestatecolor = Color.red;
		else
			shadestatecolor = Color.blue;
		if (oldcolor != shadestatecolor)
			repaint();

		// if mouse is in the bottom-right corner, than change the cull state color to red, else blue
		oldcolor = viewstatecolor;
		if ((x>size().width-90)&&(y>270))
			cullstatecolor = Color.red;
		else
			cullstatecolor = Color.blue;
		if (oldcolor != cullstatecolor)
			repaint();

		return true;
	}
	
	float v0x, v0y, v0z;

    public boolean mouseUp(Event e, int x, int y)
    {
		repaint();
		return true;
	}
    
	public boolean mouseDown(Event e, int x, int y)
    {
		if (statecolor == Color.red)
		{
			// if click on state, then increment state
			state++;
			if (state>NUMSTATE)
				state = 1;
			repaint();
		}
		else if (viewstatecolor == Color.red)
		{
			// if click on view state, then change view state
			changeView();
			display();
		}
		else if (shadestatecolor == Color.red)
		{
			// if click on shade state, then change shade state
			if (shadestate == DOTS)
			{
				shadestate = FLAT;
				triList[0].bDots = false;
				triList[0].bGouraud = false;
			}
			else if (shadestate == FLAT)
			{
				shadestate = GOURAUD;
				triList[0].bDots = false;
				triList[0].bGouraud = true;
			}
			else if (shadestate == GOURAUD)
			{
				shadestate = DOTS;
				triList[0].bDots = true;
			}
			display();
		}
		else if (cullstatecolor == Color.red)
		{
			// if click on cull state, then change cull state
			cull = !cull;
			display();
		}
		else
		{
			if (e.metaDown() && (e.clickCount>1))
			{
				// if double right click, then reset model
				showStatus("Resetting model matrix");
				model.loadIdentity();
				display();
			}
			else
			{
				v0x = (float) (x - (size().width / 2));
				v0y = (float) ((size().height / 2) - y);
				v0z = (float) size().width;
				
				switch (state)
				{
				case ROTATION:
					float l0 = (float) (1 / Math.sqrt(v0x*v0x + v0y*v0y + v0z*v0z));
					v0x *= l0;
					v0y	*= l0;
					v0z *= l0;
					break;
				case TRANSLATION:
					break;
				case SCALE:
					break;
				case SKEW:
					break;
				}
			}
		}
		return true;
    }

	public boolean mouseDrag(Event e, int x, int y)
    {
		if ((statecolor == Color.blue) && (viewstatecolor == Color.blue) && (shadestatecolor == Color.blue) && (cullstatecolor == Color.blue))
		{
			// if none of the states are lit, then change model accordingly and draw
			float v1x = (float) (x - (size().width / 2));
			float v1y = (float) ((size().height / 2) - y);
			float v1z = (float) size().width;
			
			// the transformation procedures in Matrix3D multiplies matrixes on the right side
			// we need to do the opposite to make it so that each consecutive transformation is done
			// one after the other
			// so, store the old model in a temp Matrix3D, and have a new identity model do the transform
			// and then compose the old matrix.
			Matrix3D temp = model;
			model = new Matrix3D();

			if (!e.metaDown())
			{
				// if left drag, then do transformation on the x and y axis
				switch (state)
				{
				case ROTATION:
					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;
					model.rotate(ax, ay, az, theta);
					break;
				case TRANSLATION:
					model.translate((v1x-v0x)/20f, (v1y-v0y)/20f, 0f);
					break;
				case SCALE:
					model.scale(1f + (v1x-v0x)/(float)size().width,1f + (v1y-v0y)/(float)size().height, 1f);
					break;
				case SKEW:
					model.skew((v1x-v0x)/100f,(v1y-v0y)/100f,0f);
					break;
				}
			}
			else
			{
				// if right-drag, then do transformation on the z axis
				switch (state)
				{
				case ROTATION:
					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;
					model.rotate(ax, ay, az, theta);
					break;
				case TRANSLATION:
					model.translate(0f, 0f, (v1x-v0x)/20f);
					break;
				case SCALE:
					model.scale(1f, 1f, 1f + (v1x-v0x)/(float)size().width);
					break;
				case SKEW:
					model.skew(0f, 0f, (v1x-v0x)/100f);
					break;
				}
			}
			
			model.compose(temp);
			
			v0x = v1x;
			v0y = v1y;
			v0z = v1z;
			display();
		}
        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);
        for (int i = 0; i < triangles; i++)
		{
            if (viewstate == PERSPECTIVE)
				triList[i].Illuminate(lightList, lights, eye, cull, null);
			else
				triList[i].Illuminate(lightList, lights, eye, cull, lookat);
        }
        
        /*
            .... 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].isVisible())
			{
                triList[i].ClipAndDraw(raster, project);
            }
        }
        time = System.currentTimeMillis() - time;
        showStatus("Time = "+time+" ms");
        screen = raster.toImage( );
        repaint();
    }

}