import java.awt.Color;
import java.util.Vector;
import java.util.Enumeration;
import Vertex3D;
import Matrix3D;
import Surface;
import ZRaster;

class EdgeEqn {
    public float A, B, C;
    public int flag;    

    public EdgeEqn(Vertex3D v0, Vertex3D v1)
    {
        A = v0.y - v1.y;
        B = v1.x - v0.x;
        C = -0.5f * (A*(v0.x + v1.x) + B*(v0.y + v1.y));

        flag = 0;
        if (A >= 0) flag += 8;
        if (B >= 0) flag += 1;
    }

    public void flip()
    {
        A = -A;
        B = -B;
        C = -C;
    }

    public float evaluate(int x, int y)
    {
        return (A*x + B*y + C);
    }
}

public class Triangle {
    private static Vertex3D vlist[];
	public static boolean bDots = false;
	public static boolean bGouraud = true;
    protected int v[];
    protected float r[], g[], b[];
    protected Surface surface;
    private boolean culled;
    private Vertex3D vert[];

    public Triangle()
    {
    }

    public Triangle(int v0, int v1, int v2)
    {
        v = new int[3];
        v[0] = v0;
        v[1] = v1;
        v[2] = v2;    
        r = new float[3];
        g = new float[3];
        b = new float[3];
        vert = new Vertex3D[3];
        vert[0] = new Vertex3D();
        vert[1] = new Vertex3D();
        vert[2] = new Vertex3D();
        scale = -1;
        culled = false;
    }
    
    public void setSurface(Surface s)
    {
        surface = s;
    }

    public Surface getSurface()
    {
        return surface;
    }

    public boolean isVisible()
    {
        return (!culled);
    }
    
	// The lookat is set, then use N dot lookat (if we want to view orthographic)
	//	otherwise take N dot V (where V points from eye to surface)
    public void Illuminate(Light l[], int lights, Point3D eye, boolean cull, Point3D lookat)
    {
		int v0 = v[0];
		int v1 = v[1];
		int v2 = v[2];
		
		Point3D p1 = new Point3D(vlist[v0].x - vlist[v1].x,
			vlist[v0].y - vlist[v1].y,
			vlist[v0].z - vlist[v1].z);
		Point3D p2 = new Point3D(vlist[v1].x - vlist[v2].x,
			vlist[v1].y - vlist[v2].y,
			vlist[v1].z - vlist[v2].z);
		
		Point3D pN = Matrix3D.cross(p1, p2);
		
		float vcx, vcy, vcz;
		vcx = (vlist[v0].x + vlist[v1].x + vlist[v2].x) / 3.0f;
		vcy = (vlist[v0].y + vlist[v1].y + vlist[v2].y) / 3.0f;
		vcz = (vlist[v0].z + vlist[v1].z + vlist[v2].z) / 3.0f;
		Point3D pV = new Point3D(eye.x - vcx,
			eye.y - vcy,
			eye.z - vcz);
		
		// Backface culling.
		culled = false;
		if (cull)
		{
			Point3D pVcull;
			// If orthogonal view, then must use V as the directional view,
			//	not a direct view towards the point
			if (lookat == null)
			{
				pVcull = pV;
			}
			else
			{
				pVcull = new Point3D(eye.x - lookat.x,
					eye.y - lookat.y,
					eye.z - lookat.z);
			}
			
			if (Matrix3D.dot(pN, pVcull)<0)
			{
				culled = true;
				return;
			}
		}
		
		for (int i = 0; i < 3; i++)
		{
			float ra = 0, rd = 0;
			float ga = 0, gd = 0;
			float ba = 0, bd = 0;
			
			for (int j = 0; j < lights; j++)
			{
				switch (l[j].lightType)
				{
				case (Light.AMBIENT):
					ra = surface.ka * l[j].ir;
					ga = surface.ka * l[j].ig;
					ba = surface.ka * l[j].ib;
					break;
				case (Light.DIRECTIONAL):
					// Let user turn on/off gouraud shading (it it's flat shading or just dots)
					if (bGouraud && !bDots && vlist[v[i]].hasNormal)
					{
						pN = new Point3D(vlist[v[i]].nx, vlist[v[i]].ny, vlist[v[i]].nz);

						pV = new Point3D(eye.x - vlist[v[i]].x,
							eye.y - vlist[v[i]].y,
							eye.z - vlist[v[i]].z);
					}
					pN.unitize();
					pV.unitize();

					Point3D pL = new Point3D(l[j].x, l[j].y, l[j].z);
					pL.unitize();
					float dNdotL = Math.abs(Matrix3D.dot(Matrix3D.normalize(pN), Matrix3D.normalize(pL)));
					float d2NdotL = 2 * dNdotL;
					Point3D pR = new Point3D(d2NdotL * pN.x - pL.x,
						d2NdotL * pN.y - pL.y,
						d2NdotL * pN.z - pL.z);
					pR.unitize();
					float dVdotR = Math.abs(Matrix3D.dot(Matrix3D.normalize(pV), Matrix3D.normalize(pR)));
					
					float val = (float) (surface.kd * dNdotL + surface.ks * Math.pow(dVdotR, surface.ns));
					rd += (float)(l[j].ir * val);
					gd += (float)(l[j].ig * val);
					bd += (float)(l[j].ib * val);
					break;
				}
			}
	
			r[i] = surface.r * (ra + rd);
			g[i] = surface.g * (ga + gd);
			b[i] = surface.b * (ba + bd);

			if (r[i]<.4 && g[i]<.4 && b[i]<.4)
				System.out.println("hi");
			if (r[i]>1 || g[i]>1 && b[i]>1)
				System.out.println("hi");

		}
    }    

	private boolean isFrontInside(Vertex3D v)
	{
		return (v.z >= -1);
	}

	private boolean isBackInside(Vertex3D v)
	{
		return (v.z <= 1);
	}

	private Vertex3D findIntersection(Vertex3D v1, Vertex3D v2, float fPlane)
	{
		float ratio = (fPlane - v1.z) / (v2.z - v1.z);
		Vertex3D v = new Vertex3D(v1.x + ratio * (v2.x - v1.x), v1.y + ratio * (v2.y - v1.y), fPlane);

		if (v1.hasNormal && v2.hasNormal)
		{
			float nx1 = v1.nx;
			float ny1 = v1.ny;
			float nz1 = v1.nz;

			float nx2 = v2.nx;
			float ny2 = v2.ny;
			float nz2 = v2.nz;
		
			float nx = nx1 + ratio * (nx2 - nx1);
			float ny = ny1 + ratio * (ny2 - ny1);
			float nz = nz1 + ratio * (nz2 - nz1);

			v.setNormal(nx, ny, nz);
			v.setNormal(1, 1, 1);
		}

		return v;
	}

	private Vertex3D findFrontIntersection(Vertex3D v1, Vertex3D v2)
	{
		return findIntersection(v1, v2, -1);
	}

	private Vertex3D findBackIntersection(Vertex3D v1, Vertex3D v2)
	{
		return findIntersection(v1, v2, 1);
	}

    public void ClipAndDraw(ZRaster raster, Matrix3D project)
    {
		// First normalize all vertexs
		vlist[v[0]].normalize();
		vlist[v[1]].normalize();
		vlist[v[2]].normalize();

		// Only clip against the z axis,
		//	b/c the triangle draw routines already clip against the x and y axis
		// So clip against z=-1, and z=1
		Vector vec = new Vector();
		vec.addElement(vlist[v[0]]);
		vec.addElement(vlist[v[1]]);
		vec.addElement(vlist[v[2]]);
		vec.addElement(vlist[v[0]]);

		// Front clipping
		Enumeration e = vec.elements();
		Vertex3D v1 = (Vertex3D) e.nextElement();
		Vertex3D v2;
		boolean bAddedVertex = false;
		Vector vec2 = new Vector();
		boolean b1in = isFrontInside(v1);
		boolean b2in;
		for (;e.hasMoreElements();)
		{
			v2 = (Vertex3D) e.nextElement();
            b2in = isFrontInside(v2);

			if (b1in && b2in)
			{
				if (!bAddedVertex)
					vec2.addElement(v1);
				vec2.addElement(v2);
				bAddedVertex = true;
			}
			else if (!b1in && !b2in)
			{
			}
			else
			{
				if (b1in && !bAddedVertex)
					vec2.addElement(v1);
				vec2.addElement(findFrontIntersection(v1, v2));
				if (b2in)
					vec2.addElement(v2);
				bAddedVertex = true;
			}
			b1in = b2in;
			v1 = v2;
		}

		// Back clipping
		e = vec2.elements();
		if (!e.hasMoreElements())
			return;
		v1 = (Vertex3D) e.nextElement();
		bAddedVertex = false;
		vec = new Vector();
		b1in = isBackInside(v1);
		for (;e.hasMoreElements();)
		{
			v2 = (Vertex3D) e.nextElement();
            b2in = isBackInside(v2);

			if (b1in && b2in)
			{
				if (!bAddedVertex)
					vec.addElement(v1);
				vec.addElement(v2);
				bAddedVertex = true;
			}
			else if (!b1in && !b2in)
			{
			}
			else
			{
				if (b1in && !bAddedVertex)
					vec.addElement(v1);
				vec.addElement(findBackIntersection(v1, v2));
				if (b2in)
					vec.addElement(v2);
				bAddedVertex = true;
			}
			b1in = b2in;
			v1 = v2;
		}

		e = vec.elements();
		if (!e.hasMoreElements())
			return;
		v1 = (Vertex3D) e.nextElement();
		if (!e.hasMoreElements())
			return;
		v2 = (Vertex3D) e.nextElement();
		while (e.hasMoreElements())
		{
			Vertex3D v3 = (Vertex3D) e.nextElement();
			if (v3 == v1) continue;
			vert[0] = project.transform(v1);
			vert[1] = project.transform(v2);
			vert[2] = project.transform(v3);
			Draw(raster);
			v2 = v3;
		}
    }

    public void setVertexList(Vertex3D list[])
    {
        vlist = list;
    }

    protected EdgeEqn edge[];
    protected float area;
    protected int xMin, xMax, yMin, yMax;
    protected float scale;
    private static byte sort[][] = {
        {0, 1}, {1, 2}, {0, 2}, {2, 0}, {2, 1}, {1, 0}
    };

    public void PlaneEqn(float eqn[], float p0, float p1, float p2)
    {
        float Ap, Bp, Cp;

        float sp0 = scale * p0;
        float sp1 = scale * p1;
        float sp2 = scale * p2;
        Ap = edge[0].A*sp2 + edge[1].A*sp0 + edge[2].A*sp1;
        Bp = edge[0].B*sp2 + edge[1].B*sp0 + edge[2].B*sp1;
        Cp = edge[0].C*sp2 + edge[1].C*sp0 + edge[2].C*sp1;
        eqn[0] = Ap;
        eqn[1] = Bp;
        eqn[2] = Ap*xMin + Bp*yMin + Cp;
    }

    protected boolean triangleSetup(Raster r)
    {
		try{
        if (edge == null) edge = new EdgeEqn[3];

        /*
            Compute the three edge equations
        */
        edge[0] = new EdgeEqn(vert[0], vert[1]);
        edge[1] = new EdgeEqn(vert[1], vert[2]);
        edge[2] = new EdgeEqn(vert[2], vert[0]);

        /*
            Trick #1: Orient edges so that the
            triangle's interior lies within all
            of their positive half-spaces.

            Assuring that the area is positive
            accomplishes this
        */
        area = edge[0].C + edge[1].C + edge[2].C;

        if (area == 0)
            return false;
            
        if (area < 0) {
            edge[0].flip();
            edge[1].flip();
            edge[2].flip();
            area = -area;
        }

        /*
            Trick #2: compute bounding box
        */
        int xflag = edge[0].flag + 2*edge[1].flag + 4*edge[2].flag;
        int yflag = (xflag >> 3) - 1;
        xflag = (xflag & 7) - 1;

        xMin = (int) (vert[sort[xflag][0]].x);
        xMax = (int) (vert[sort[xflag][1]].x + 1);
        yMin = (int) (vert[sort[yflag][1]].y);
        yMax = (int) (vert[sort[yflag][0]].y + 1);

        /*
            clip triangle's bounding box to raster
        */
        xMin = (xMin < 0) ? 0 : xMin;
        xMax = (xMax >= r.width) ? r.width - 1 : xMax;
        yMin = (yMin < 0) ? 0 : yMin;
        yMax = (yMax >= r.height) ? r.height - 1 : yMax;
		}catch (Exception e)
		{
			System.out.println("Error");
		}
        return true;
    }
    
    public void Draw(ZRaster raster)
    {
		if (bDots)
		{
			for (int i = 0; i < 3; i++)
			{
				if ((vert[i].x < 0) || (vert[i].x >= raster.width) || (vert[i].y < 0) || (vert[i].y >= raster.height))
					continue;
				int iz = (int) vert[i].z;
				int point = (int)(vert[i].y+0.5) * raster.width + (int)(vert[i].x+0.5);
				if (iz <= raster.zbuff[point])
				{
					int pixr = (int) (255.0f*r[i]);
					int pixg = (int) (255.0f*g[i]);
					int pixb = (int) (255.0f*b[i]);
					pixr = ((pixr & ~255) == 0) ? pixr << 16 : ((r[i] < 0)	? 0 : 255<<16);
					pixg = ((pixg & ~255) == 0) ? pixg << 8  : ((g[i] < 0)	? 0 : 255<<8);
					pixb = ((pixb & ~255) == 0) ? pixb       : ((b[i] < 0)	? 0 : 255);
					raster.pixel[point] = (0xff000000 | pixr | pixg | pixb);
					raster.zbuff[point] = iz;
				}
			}
		}
		else
		{
			float zPlane[] = new float[3];
			float rPlane[] = new float[3];
			float gPlane[] = new float[3];
			float bPlane[] = new float[3];
			
			if (!triangleSetup(raster)) return;
			scale = 1 / area;
			PlaneEqn(zPlane, vert[0].z, vert[1].z, vert[2].z);
			PlaneEqn(rPlane, r[0], r[1], r[2]);
			PlaneEqn(gPlane, g[0], g[1], g[2]);
			PlaneEqn(bPlane, b[0], b[1], b[2]);
			
			int x, y;
			float A0 = edge[0].A;        float B0 = edge[0].B;        float t0 = A0*xMin + B0*yMin + edge[0].C;
			float A1 = edge[1].A;        float B1 = edge[1].B;        float t1 = A1*xMin + B1*yMin + edge[1].C;
			float A2 = edge[2].A;        float B2 = edge[2].B;        float t2 = A2*xMin + B2*yMin + edge[2].C;
			float Az = zPlane[0];        float Bz = zPlane[1];        float tz = zPlane[2];
			float Ar = rPlane[0];        float Br = rPlane[1];        float tr = rPlane[2];
			float Ag = gPlane[0];        float Bg = gPlane[1];        float tg = gPlane[2];
			float Ab = bPlane[0];        float Bb = bPlane[1];        float tb = bPlane[2];
			
			yMin *= raster.width;
			yMax *= raster.width;
			
			/*
			.... scan convert triangle ....
			*/
			for (y = yMin; y <= yMax; y += raster.width) {
				float e0 = t0;
				float e1 = t1;
				float e2 = t2;
				float r = tr;
				float g = tg;
				float b = tb;
				float z = tz;
				boolean beenInside = false;
				for (x = xMin; x <= xMax; x++) {
					if ((e0 >= 0) && (e1 >= 0) && (e2 >= 0)) {       // all 3 edges must be >= 0
						int iz = (int) z;
						if (iz <= raster.zbuff[y+x]) {
							int pixr = (int) (255.0f*r);
							int pixg = (int) (255.0f*g);
							int pixb = (int) (255.0f*b);
							pixr = ((pixr & ~255) == 0) ? pixr << 16 : ((r < 0)	? 0 : 255<<16);
							pixg = ((pixg & ~255) == 0) ? pixg << 8  : ((g < 0)	? 0 : 255<<8);
							pixb = ((pixb & ~255) == 0) ? pixb       : ((b < 0)	? 0 : 255);
							raster.pixel[y+x] = (0xff000000 | pixr | pixg | pixb);
							raster.zbuff[y+x] = iz;
						}
						beenInside = true;
					} else if (beenInside) break;
					e0 += A0;   e1 += A1;   e2 += A2;
					z += Az;    r += Ar;    g += Ag;    b += Ab;
				}
				t0 += B0;   t1 += B1;   t2 += B2;
				tz += Bz;   tr += Br;   tg += Bg;   tb += Bb;
			}
		}
    }
}
