import java.awt.*;
import java.util.*;
import java.lang.Math;

public class Shading extends MISApplet
{
	private final double focus = 400;
	private final double EPSILON = 1.0 / 20.0;
	private final double RADIUS = 25;
	private final int EPSILON_INV = 20;
	
	private final int X = 0;
	private final int Y = 1;
	private final int Z = 2;

	private final int R = 0;
	private final int G = 1;
	private final int B = 2;
	
	private final int TOP = 0;
	private final int MID = 1;
	private final int BOT = 2;	
	
	private final int PRE_DRAW = 1;
	private final int POST_DRAW = -1;
	
	private final int RGB = 255;
	
	ParamSphere Sphere[] = new ParamSphere[5];
	ParamSphere Shadow[] = new ParamSphere[5];
	ParamInfinity Infinity;
	
	Matrix3D Matrix3D = new Matrix3D();
	double mStack[][][] = new double[100][4][4];
	double MatrixTemp[][] = new double[4][4];
	int mTop = 0;

	double dSurfaceNormal[][][];
	double dVertexNormal[][][];
	double dNormalized[][][];
	
	double u[] = new double[3];
	double v[] = new double[3];
	double t;
		
	VertexNode Triangle[] = new VertexNode[3];
	VertexNode Vertices[] = new VertexNode[4];
	VertexNode Trap1[] = new VertexNode[4];
	VertexNode Trap2[] = new VertexNode[4];
	VertexNode NewNode = new VertexNode();
	VertexNode LeftNode = new VertexNode();
	VertexNode RightNode = new VertexNode();

	Button BtnAmbience;
	Slider SldR;
	Slider SldG;
	Slider SldB;
	Slider SldGravity;
	
	int iTopMidBot[];
	
	private int iHeight, iWidth;
	private int iX, iY;
	private int FrameBuffer[][];
	private double ZBuffer[][];
	private double dLight[] = {-1000,1500,-1000};
	private double dAmbience[] = {0.1, 0.1, 0.1};
	private Color clrAmbience = new Color((float)0.9,(float)0.9,(float)0.9);
	
	double dStartTime;
	double CameraX, CameraY;
	double dTheta;
	boolean bIsInited = false;
	boolean bCameraView = false;
	
	public void render(Graphics g, double dCurrentTime)
	{
		if (bIsInited == false)
		{
			dStartTime = dCurrentTime;
			InitStuff();
			
			Infinity = new ParamInfinity();
			
			bIsInited = true;
		}	
		
		//clear the FrameBuffer and the ZBuffer
		for (int i=0; i<iHeight; i++)
			for (int j=0; j<iWidth; j++)
			{
				FrameBuffer[i][j] = pack(230,230,230);
				ZBuffer[i][j] = 1;
			}
		
		//re-init the sphere every cycle
		for (int i=0; i<5; i++)
			Sphere[i] = new ParamSphere(0, 0, 0, RADIUS, EPSILON, iHeight, iWidth);
		
		//Initial position for balls
		Matrix3D.IdentityMatrix(mStack[mTop]);
		Sphere[0].Translate(mStack[mTop], 0, 100, 50);
		
		g.setColor(Color.white);
//		g.fillRect(0,0,iWidth, iHeight);


		//position each correctly
		push();
		{
			//center ball
			Sphere[2].Transform(mStack[mTop], Sphere[2].getShape());
			DrawShade(g, Sphere[2]);
		}
		pop();
		
		push();
		{
			//second ball from left
			Sphere[1].Translate(mStack[mTop], -2*RADIUS, 0, 0);
			Sphere[1].Transform(mStack[mTop], Sphere[1].getShape());
			DrawShade(g, Sphere[1]);
		}
		pop();
		
		push();
		{
			//fourth ball from left
			Sphere[3].Translate(mStack[mTop], 2*RADIUS, 0, 0);
			Sphere[3].Transform(mStack[mTop], Sphere[3].getShape());
			DrawShade(g, Sphere[3]);
		}
		pop();

		//which should rotate?
		int iRotate=0, iNoRotate=0, iDir=0;
//		dCurrentTime = dCurrentTime/2;
		dTheta = (1-SldGravity.getValue()) * (Math.sin(dCurrentTime));
		
		if (dTheta < 0)
		{
			iRotate = 0;
			iNoRotate = 4;
			iDir = -1;
		}
		else if (dTheta > 0)
		{
			iRotate = 4;
			iNoRotate = 0;
			iDir = 1;
		}

		push();
		{	
			Sphere[iRotate].Transform(mStack[mTop], Sphere[iRotate].getShape());
			
			Matrix3D.IdentityMatrix(mStack[mTop]);
			
			Sphere[iRotate].Translate(mStack[mTop], iDir*4*RADIUS, 0,0);
			Sphere[iRotate].Rotate(mStack[mTop], -1.5*dTheta, Z);
			Sphere[iRotate].Transform(mStack[mTop], Sphere[iRotate].getShape());
			
			DrawShade(g, Sphere[iRotate]);
		}
		pop();

		push();
		{	
			Sphere[iNoRotate].Translate(mStack[mTop], -iDir*4*RADIUS, 0,0);

			Sphere[iNoRotate].Transform(mStack[mTop], Sphere[iNoRotate].getShape());
			DrawShade(g, Sphere[iNoRotate]);
		}
		pop();
			
		//draw from FrameBuffer
/*		for (int i=0; i<iHeight; i++)
		{
			for (int j=0; j<iWidth; j++)
			{
				g.setColor(FrameBuffer[i][j]);
				g.fillRect(j,i,1,1);
			}
		}
		
		//shadows?
		for (int h=0; h<3; h++)
		{
			for (int i=0; i<5; i++)
			{
				Shadow[i] = new ParamSphere(0,0,0,RADIUS, EPSILON, iHeight, iWidth);
				Matrix3D.IdentityMatrix(mStack[mTop]);
				Shadow[i].Scale(mStack[mTop], 0.9 + (3-h)*0.1,0.25, 0.9 + (3-h)*0.1);
				Shadow[i].Transform(mStack[mTop],Shadow[i].getShape());
				
				Matrix3D.IdentityMatrix(mStack[mTop]);
				push();
					Shadow[i].Translate(mStack[mTop], Sphere[i].getCenter()[X] - Shadow[i].getCenter()[X]-RADIUS, 7*RADIUS, 0);
					Shadow[i].Transform(mStack[mTop], Shadow[i].getShape());
					Shadow[i].draw(g, EPSILON, new Color(0,0,0,32),0);
				pop();
			}
		}
				
		//Buttons and sliders
		BtnAmbience.render(g);
		SldR.render(g);
		SldG.render(g);
		SldB.render(g);
		SldGravity.render(g);
*/		
		computeImage(System.currentTimeMillis() - dStartTime, FrameBuffer);
	}
	
	public void InitStuff()
	{
		iHeight = bounds().height;
		iWidth = bounds().width;
		
		FrameBuffer = new int[iHeight+1][iWidth+1];
		ZBuffer = new double[iHeight+1][iWidth+1];
					
		CameraX = 0;
		CameraY = 0;
		
		dSurfaceNormal = new double[iHeight][iWidth][3];
		dVertexNormal = new double[iHeight][iWidth][3];
		dNormalized = new double[iHeight][iWidth][3];

		BtnAmbience = new Button(5,5,60, 20);
		BtnAmbience.setValue(0);
		String sLinesLabels[] = {"Amb Off", "Amb On"};
		BtnAmbience.setLabel(sLinesLabels);
		
		SldR = new Slider(75, 5, 90, 20);
		SldR.setValue(0.9);
		SldR.setLabel("Red");
		
		SldG = new Slider(175, 5, 90, 20);
		SldG.setValue(0.9);
		SldG.setLabel("Green");

		SldB = new Slider(275, 5, 90, 20);
		SldB.setValue(0.9);
		SldB.setLabel("Blue");

		SldGravity = new Slider(375, 5, 90, 20);
		SldGravity.setValue(0.5);
		SldGravity.setLabel("Gravity");		

		for (int k=0; k<3; k++)
			Triangle[k] = new VertexNode();
		for (int k=0; k<4; k++)
			Vertices[k] = new VertexNode();
		for (int k=0; k<4; k++)
		{
			Trap1[k] = new VertexNode();
			Trap2[k] = new VertexNode();
		}
	}
	
	public void DrawShade(Graphics g, ParamSphere Surface)
		//should send this a matrix of the xyz coords instead of the actual shape
		//this will help generalize it
		//also, change the shapes so that they store xyz in a 2D matrix
	{
		for (int i=0; i<EPSILON_INV; i++)
			//get the surface normals of the polygons
		{
			for (int j=0; j<EPSILON_INV; j++)
			{
				u[X] = Surface.getShape()[i+1][j][X] - Surface.getShape()[i][j][X];
				u[Y] = Surface.getShape()[i+1][j][Y] - Surface.getShape()[i][j][Y];
				u[Z] = Surface.getShape()[i+1][j][Z] - Surface.getShape()[i][j][Z];
				
				//if i=0, do its so you get the 'parallel' sides
				
				v[X] = Surface.getShape()[i][(j+1)%EPSILON_INV][X] - Surface.getShape()[i][j][X];
				v[Y] = Surface.getShape()[i][(j+1)%EPSILON_INV][Y] - Surface.getShape()[i][j][Y];
				v[Z] = Surface.getShape()[i][(j+1)%EPSILON_INV][Z] - Surface.getShape()[i][j][Z];

				dSurfaceNormal[i][j] = CrossProduct(u,v);
			}
		}
		
		for (int i=0; i<EPSILON_INV; i++)
			//get the surface normals of the vertices by getting the average
			//of the surface normals of the polygons surrounding it
			//normalize the surface normals
		{
			for (int j=0; j<EPSILON_INV; j++)
			{
				if(i==0)
				{
					for(int k=0; k<EPSILON_INV; k++)
					{
						dVertexNormal[i][j][X] += dSurfaceNormal[i][k%EPSILON_INV][X];
						dVertexNormal[i][j][Y] += dSurfaceNormal[i][k%EPSILON_INV][Y];
						dVertexNormal[i][j][Z] += dSurfaceNormal[i][k%EPSILON_INV][Z];
					}
					i++;					
				}
				dVertexNormal[i][j][X] = dSurfaceNormal[i][j][X] + 
										 dSurfaceNormal[i][(j+1)%EPSILON_INV][X] +
										 dSurfaceNormal[i+1][(j+1)%EPSILON_INV][X] + 
										 dSurfaceNormal[i+1][j][X];
													
				dVertexNormal[i][j][Y] = dSurfaceNormal[i][j][Y] + 
										 dSurfaceNormal[i][(j+1)%EPSILON_INV][Y] +
										 dSurfaceNormal[i+1][(j+1)%EPSILON_INV][Y] + 
										 dSurfaceNormal[i+1][j][Y];
													
				dVertexNormal[i][j][Z] = dSurfaceNormal[i][j][Z] + 
										 dSurfaceNormal[i][(j+1)%EPSILON_INV][Z] +
										 dSurfaceNormal[i+1][(j+1)%EPSILON_INV][Z] + 
										 dSurfaceNormal[i+1][j][Z];

				dNormalized[i][j] = Normalize(dVertexNormal[i][j]);
			}
		}


		for (int i=0; i<EPSILON_INV; i++)
			//for each polygon
		{
			for (int j=0; j<EPSILON_INV; j++)
			{
				
				Vertices[0].setXYZ(Surface.getShape()[i][j]);
				Vertices[0].setRGB(Shade(dNormalized[i][j]));
				
				Vertices[1].setXYZ(Surface.getShape()[i][(j+1)%EPSILON_INV]);
				Vertices[1].setRGB(Shade(dNormalized[i][(j+1)%EPSILON_INV]));
				
				Vertices[2].setXYZ(Surface.getShape()[i+1][(j+1)%EPSILON_INV]);
				Vertices[2].setRGB(Shade(dNormalized[i+1][(j+1)%EPSILON_INV]));
				
				Vertices[3].setXYZ(Surface.getShape()[i+1][j]);
				Vertices[3].setRGB(Shade(dNormalized[i+1][j]));
				
				for (int k=0; k<4; k++)
				{
					double dTemp[] = new double[3];
				
					//get px, py, pz
					dTemp[X] = (focus * Vertices[k].getXYZ()[X]) / (focus - Vertices[k].getXYZ()[Z]);
					dTemp[Y] = (focus * Vertices[k].getXYZ()[Y]) / (focus - Vertices[k].getXYZ()[Z]);
					dTemp[Z] = 1 / Vertices[k].getXYZ()[Z];
					
					//viewport
					dTemp[X] = dTemp[X] + iWidth/2;
					dTemp[Y] = dTemp[Y] + iHeight/2;
					
					//these vertices contain RGB values and px, py, pz
					Vertices[k].setXYZ(dTemp);
				}
				
				//first triangle
				Triangle[0].Copy(Vertices[0]);
				Triangle[1].Copy(Vertices[1]);
				Triangle[2].Copy(Vertices[2]);
				
				ScanConvert(g, Triangle);
				
				Triangle[0].Copy(Vertices[0]);
				Triangle[1].Copy(Vertices[2]);
				Triangle[2].Copy(Vertices[3]);
								
				ScanConvert(g, Triangle);
			}
		}
	}
		
	public void ScanConvert(Graphics g, VertexNode[] Triangle)
		//this takes a triangle, splits it into trapezoids
		//lerps to get individual pixel stuffs, etc, you know
	{
		//iTemp[0] has the max, iTemp[1] has the mid, iTemp[2] has the min
		iTopMidBot = MaxMin(Triangle);
		
		//we have three separate thingers so we have to make a new vertexnode
		t = (Triangle[iTopMidBot[MID]].getXYZ()[Y] - Triangle[iTopMidBot[BOT]].getXYZ()[Y]) / 
				(Triangle[iTopMidBot[TOP]].getXYZ()[Y] - Triangle[iTopMidBot[BOT]].getXYZ()[Y]);

		NewNode.Copy(VertexLerp(t, Triangle[iTopMidBot[BOT]], Triangle[iTopMidBot[TOP]]));
		
		if (NewNode.getXYZ()[X] < Triangle[iTopMidBot[MID]].getXYZ()[X])
		{
			Trap1[0].Copy(Triangle[iTopMidBot[TOP]]);
			Trap1[1].Copy(NewNode);
			Trap1[2].Copy(Triangle[iTopMidBot[MID]]);
			Trap1[3].Copy(Triangle[iTopMidBot[TOP]]);

			Trap2[0].Copy(NewNode);
			Trap2[1].Copy(Triangle[iTopMidBot[BOT]]);
			Trap2[2].Copy(Triangle[iTopMidBot[BOT]]);
			Trap2[3].Copy(Triangle[iTopMidBot[MID]]);
		}
		else
		{
			Trap1[0].Copy(Triangle[iTopMidBot[TOP]]);
			Trap1[1].Copy(Triangle[iTopMidBot[MID]]);
			Trap1[2].Copy(NewNode);
			Trap1[3].Copy(Triangle[iTopMidBot[TOP]]);

			Trap2[0].Copy(Triangle[iTopMidBot[MID]]);
			Trap2[1].Copy(Triangle[iTopMidBot[BOT]]);
			Trap2[2].Copy(Triangle[iTopMidBot[BOT]]);
			Trap2[3].Copy(NewNode);
		}				

		DrawTrapezoid(Trap1);
		DrawTrapezoid(Trap2);
	}
	
	public void DrawPoly(Graphics g, VertexNode[] Poly)
	{
		int x[] = new int[Poly.length];
		int y[] = new int[Poly.length];
		for (int i=0; i< Poly.length; i++)
		{
			x[i] = (int)Poly[i].getXYZ()[X];
			y[i] = (int)Poly[i].getXYZ()[Y];
		}
		Color temp = new Color((int)(255*Poly[0].getRGB()[R]), (int)(255*Poly[0].getRGB()[G]), (int)(255*Poly[0].getRGB()[B]));
		g.setColor(temp);
		
		if (BtnAmbience.getValue()==0)
			g.fillPolygon(x, y, Poly.length);
		else
			g.drawPolygon(x, y, Poly.length);
	}
	
	public void DrawTrapezoid(VertexNode[] Trap)
	{
		int top = (int)Math.ceil(Trap[0].getXYZ()[Y]);
		int bottom = (int)Math.ceil(Trap[1].getXYZ()[Y]);
		
		double ti, tj;
		for (int i = bottom; i < top; i++)
		{
			//find t between absolute top and current scanline
			//interpolate to get left and right
			ti = (i - Trap[1].getXYZ()[Y]) / (Trap[0].getXYZ()[Y] - Trap[1].getXYZ()[Y]);
			
			LeftNode.Copy(VertexLerp(ti, Trap[1], Trap[0]));
			RightNode.Copy(VertexLerp(ti, Trap[2], Trap[3]));
			//do it again between left and right
			
			int left = (int)Math.ceil(LeftNode.getXYZ()[X]);
			int right = (int)Math.ceil(RightNode.getXYZ()[X]);
			
			for (int j=left; j<right; j++)
			{
				VertexNode PixelNode = new VertexNode();;
				tj = (j - LeftNode.getXYZ()[X]) / (RightNode.getXYZ()[X] - LeftNode.getXYZ()[X]);
					
				PixelNode.Copy(VertexLerp(tj, LeftNode, RightNode));

				if (PixelNode.getXYZ()[Z] < ZBuffer[i][j])
				{
					FrameBuffer[i][j] = pack((int)(255*PixelNode.getRGB()[R]),(int)(255*PixelNode.getRGB()[G]),(int)(255*PixelNode.getRGB()[B]));
					ZBuffer[i][j] = PixelNode.getXYZ()[Z];
				}
			}			
		}		
	}
	
	public VertexNode VertexLerp(double t, VertexNode A, VertexNode B)
	{
		VertexNode NewNode = new VertexNode();
		double XYZ[] = new double[3];
		double RGB[] = new double[3];

		XYZ[X] = lerp(t, A.getXYZ()[X], B.getXYZ()[X]);
		XYZ[Y] = lerp(t, A.getXYZ()[Y], B.getXYZ()[Y]);
		XYZ[Z] = lerp(t, A.getXYZ()[Z], B.getXYZ()[Z]);
		
		RGB[R] = lerp(t, A.getRGB()[R], B.getRGB()[R]);
		RGB[G] = lerp(t, A.getRGB()[G], B.getRGB()[G]);
		RGB[this.B] = lerp(t, A.getRGB()[this.B], B.getRGB()[this.B]);
		
		NewNode.setXYZ(XYZ);
		NewNode.setRGB(RGB);
		
		return NewNode;	
	}
	
	public double[] Shade(double[] dNormal)
	{
		double RGB[] = {0,0,0};
		if (BtnAmbience.getValue() == 0)
			RGB = AddAmbience(RGB);
		RGB = AddDiffuse(RGB, dNormal);
//		RGB = AddBoring(RGB, Normal);		
		return RGB;
	}
	
	public double[] AddAmbience(double[] RGB)
	{
		for(int i=0; i<RGB.length; i++)
			RGB[i] += dAmbience[i];
		return RGB;
	}
	
	public double[] AddDiffuse(double[] RGB, double[] dNormal)
	{
		double dDiffuse[] = {SldR.getValue(), SldG.getValue(), SldB.getValue()};
		for(int i=0; i<RGB.length; i++)
			RGB[i] += 0.9 * (dDiffuse[i] * Math.max(0,DotProduct(dNormal, Normalize(dLight))));
		return RGB;
	}	
	
	public double[] AddBoring (double[] RGB, double[] dNormal)
	{
		for(int i=0; i<RGB.length; i++)
			RGB[i] = 0.5 + 0.5 * dNormal[i];
		return RGB;
	}
	
	public double[] Normalize(double[] dVector)
	{
		double dLength = Math.sqrt(DotProduct(dVector, dVector));
		dVector[X] = dVector[X] / dLength;
		dVector[Y] = dVector[Y] / dLength;
		dVector[Z] = dVector[Z] / dLength;
		
		return dVector;
	}
	
	public double[] CrossProduct(double[] u, double[] v)
	{
		double dCross[] = new double[3];
		dCross[X] = u[Y]*v[Z] - u[Z]*v[Y];
		dCross[Y] = u[Z]*v[X] - u[X]*v[Z];
		dCross[Z] = u[X]*v[Y] - u[Y]*v[X];
		
		return dCross;
	}
	
	public double DotProduct(double[] u, double[] v)
	{
		return (u[X]*v[X] + u[Y]*v[Y] +u[Z]*v[Z]);
	}
	
	public double lerp(double t, double a, double b)
	{
		return a + t * (b - a);
	}
	
	public void CameraView(int iSign, ParamSphere Sphere)
	{
		if (iSign == PRE_DRAW)
		{
			Matrix3D.IdentityMatrix(mStack[mTop]);
			push();	
				Sphere.Rotate(mStack[mTop], -CameraX, Y);
				Sphere.Transform(mStack[mTop], Sphere.getShape());
			pop();
			push();
				Sphere.Rotate(mStack[mTop], CameraY, X);
				Sphere.Transform(mStack[mTop], Sphere.getShape());
			pop();
		}
		else if (iSign == POST_DRAW)
		{
			Matrix3D.IdentityMatrix(MatrixTemp);
			push();
				Sphere.Rotate(mStack[mTop], -CameraY, X);
				Sphere.Transform(mStack[mTop], Sphere.getShape());
			pop();
			push();
				Sphere.Rotate(mStack[mTop], CameraX, Y);
				Sphere.Transform(mStack[mTop], Sphere.getShape());
			pop();
		}
	}
	
	public int[] MaxMin(VertexNode[] Triangle)
	{
		int iIndexMax = 0, iIndexMin = 0, iIndexMid = 0;
		double dMax, dMin;
		
		dMax = Math.max(Triangle[0].getXYZ()[Y], Triangle[1].getXYZ()[Y]);
		dMax = Math.max(dMax, Triangle[2].getXYZ()[Y]);

		dMin = Math.min(Triangle[0].getXYZ()[Y], Triangle[1].getXYZ()[Y]);
		dMin = Math.min(dMin, Triangle[2].getXYZ()[Y]);
		
		for (int i=0; i<Triangle.length; i++)
		{
			if (Triangle[i].getXYZ()[Y] == dMax)
				iIndexMax = i;
			else if (Triangle[i].getXYZ()[Y] == dMin)
				iIndexMin = i;
			else
				iIndexMid = i;
		}
		
		int iTemp[] = {iIndexMax, iIndexMid, iIndexMin};
		return iTemp;
	}

	public void push()
	{
		Matrix3D.Copy(mStack[mTop], mStack[mTop+1]);
		mTop++;
	}
	
	public void pop()
	{
		mTop--;
	}

	public boolean mouseDrag(Event e, int x, int y)
	{
		SldR.drag(x, y);
		SldG.drag(x, y);
		SldB.drag(x, y);
		SldGravity.drag(x,y);
/*		{		
			x = iX - x;
			y = iY - y;
			CameraX = (CameraX + ((double)x / 360.0))%(2*Math.PI);
			CameraY = (CameraY + ((double)y / 360.0))%(2*Math.PI);
		}
*/		
		return true;	
	}

	public boolean mouseUp(Event e, int x, int y)
	{
		BtnAmbience.up(x,y);
		return true;
	}
	
	public boolean mouseDown(Event e, int x, int y)
	{
		BtnAmbience.down(x,y);
		SldR.down(x, y);
		SldG.down(x, y);
		SldB.down(x, y);
		SldGravity.down(x,y);
		iX = x;
		iY = y;
		return true;			
	}
}