// FILE:     Surface.java
// PURPOSE:  Represents a surface in our ray tracing environment
// METHOD:   Holds constants which describe any surface and has the
//           ability to calculate the shading at a particular point
//           given appropriate elements of the scene.
//
// MODS:     11.25.98 -- Leonard McMillan -- original 
//           11.25.98 -- jlueck@mit.edu   -- Rev 2

import java.awt.Color;
import java.util.Enumeration;
import java.util.Vector;

public class Surface implements LightConsts {

    public float ir, ig, ib;        // surface's intrinsic color
    public float ka, kd, ks, ns;    // constants for phong model
    public float kt, kr, nt;
    private static final float TINY = 0.001f;
    private static final float I255 = 0.00392156f;  // 1/255

    public Surface(float rval, float gval, float bval, 
		   float a, float d, float s, float n, 
		   float r, float t, float index) {

        ir = rval; ig = gval; ib = bval;
        ka = a; kd = d; ks = s; ns = n;
        kr = r*I255; kt = t*I255; nt = index;
    }

    public Color Shade(Vector3D p, Vector3D n, Vector3D v, 
		       Vector lights, Vector objects, Color bgnd,
		       boolean inside) {
	    

        Enumeration lightSources = lights.elements();

        float r = 0;
        float g = 0;
        float b = 0;
        while (lightSources.hasMoreElements() && !inside) {
            Light light = (Light) lightSources.nextElement();
            if (light.lightType == AMBIENT) {
                r += ka*ir*light.ir;
                g += ka*ig*light.ig;
                b += ka*ib*light.ib;
            } else {
                Vector3D l;
                if (light.lightType == POINT) {
                    l = new Vector3D(light.lvec.x - p.x, 
				     light.lvec.y - p.y, 
				     light.lvec.z - p.z);
                    l.normalize();
                } else {
                    l = new Vector3D(-light.lvec.x, 
				     -light.lvec.y, 
				     -light.lvec.z);
                }

                // Check if the surface point is in shadow
                Vector3D poffset = new Vector3D(p.x + TINY*l.x, 
						p.y + TINY*l.y, 
						p.z + TINY*l.z);
                Ray shadowRay = new Ray(poffset, l);
                if (shadowRay.trace(objects))
                    break;

                float lambert = Vector3D.dot(n,l);
                if (lambert > 0) {
                    if (kd > 0) {
                        float diffuse = kd*lambert;
                        r += diffuse*ir*light.ir;
                        g += diffuse*ig*light.ig;
                        b += diffuse*ib*light.ib;
                    }
                    if (ks > 0) {
                        lambert *= 2;
                        float spec = v.dot(lambert*n.x - l.x, 
					   lambert*n.y - l.y, 
					   lambert*n.z - l.z);
                        if (spec > 0) {
                            spec = ks*((float) Math.pow((double) spec, 
							(double) ns));
                            r += spec*light.ir;
                            g += spec*light.ig;
                            b += spec*light.ib;
                        }
                    }
                }
            }
        }

        // Compute illumination due to reflection
	float t = v.dot(n);

	// flip N if inside of object.
	if (inside) t *= -1;
      

	if (t > 0) {
	    // reflection: only if from outside.
	    if (kr > 0 && !inside) {

                float t2 = t * 2;
                Vector3D reflect = new Vector3D(t2*n.x - v.x, 
						t2*n.y - v.y, 
						t2*n.z - v.z);
                Vector3D poffset = new Vector3D(p.x + TINY*reflect.x, 
						p.y + TINY*reflect.y, 
						p.z + TINY*reflect.z);
                Ray reflectedRay = new Ray(poffset, reflect);
		
                if (reflectedRay.trace(objects)) {
                    Color rcolor = reflectedRay.Shade(lights, objects, bgnd);
                    r += kr*rcolor.getRed();
                    g += kr*rcolor.getGreen();
                    b += kr*rcolor.getBlue();
                } else {
                    r += kr*bgnd.getRed();
                    g += kr*bgnd.getGreen();
                    b += kr*bgnd.getBlue();
                }
            }
	    
	    // refraction: additive if outside.
	    if (kt > 0) {

		float thetaI = t / (v.length() * n.length());		
		float refract = (inside) ? (1/nt) : nt;
		float thetaR = (float) Math.sqrt(1 - (refract*refract*
						      (1 - thetaI*thetaI)));
		float nfraction = thetaR - refract*thetaI;
		
		// refracted angle
      		Vector3D T = new Vector3D(-refract*v.x - nfraction*n.x,
					  -refract*v.y - nfraction*n.y,
					  -refract*v.z - nfraction*n.z);
	       
		// refracted position
		Vector3D poffset = new Vector3D(p.x + TINY*T.x,
						p.y + TINY*T.y,
						p.z + TINY*T.z);

		// refraction ray.
		Ray refractedRay = new Ray(poffset, T);
		
		// shade with kt as scaling factor
		if (refractedRay.trace(objects)) {
		    Color rcolor = refractedRay.Shade(lights, objects, bgnd);
		    r += kt*rcolor.getRed();
		    g += kt*rcolor.getGreen();
		    b += kt*rcolor.getBlue();
		} else {
		    r += kt*bgnd.getRed();
		    g += kt*bgnd.getGreen();
		    b += kt*bgnd.getBlue();
		}
	    }
	}

        r = (r > 1f) ? 1f : r;
        g = (g > 1f) ? 1f : g;
        b = (b > 1f) ? 1f : b;
        return new Color(r, g, b);
    }
}
