import java.awt.*;
import java.util.*;
import Light;
import Vector3D;
import Ray;

/****
 *
 *   An instructional Ray-Tracing Renderer written
 *   for MIT 6.837  Fall '98 by Leonard McMillan.
 *
 ****
 *
 *   Revisions to the code by Jonathan Lie, 12/1998
 *      - Reflection inside objects
 *      - Refraction
 *      - Limit on amount of recursion
 *        (to prevent stack overflow)
 *      - Minor changes here and there
 *
 ****/

class Surface {
  public float ir, ig, ib;        // surface's intrinsic color
  public float ka, kd, ks, ns;    // constants for phong model
  public float kr, kt, nt;  // fraction of ray reflected,refracted; ratio of n's
  private static final float TINY = 0.001f;
  private static final float I255 = 0.00392156f;  // 1/255
  private static int MAX_RECURSION_DEPTH = 30; // THIS IS COMPLETELY ARBITRARY

  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; nt = index;
  }

  public Color Shade(Vector3D p, Vector3D n, Vector3D v, Vector lights, Vector objects, Color bgnd, int depth) {
    Enumeration lightSources = lights.elements();

    float r = 0;
    float g = 0;
    float b = 0;
    while (lightSources.hasMoreElements()) {
      Light light = (Light) lightSources.nextElement();
      if (light.lightType == Light.AMBIENT) {
	r += ka*ir*light.ir;
	g += ka*ig*light.ig;
	b += ka*ib*light.ib;
      } else {
	Vector3D l;
	if (light.lightType == Light.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;
	    }
	  }
	}
      }
    }

    if (depth < MAX_RECURSION_DEPTH) {

      float cos_ang_in = v.dot(n);
      boolean inside = (cos_ang_in < 0);
      // Compute illumination due to reflection
      if (kr > 0) {
	//if (!inside) {
	float t = 2*cos_ang_in;
	Vector3D reflect;
	// normal should be flipped if ray is inside
	if (!inside)
	  reflect = new Vector3D(t*n.x - v.x, t*n.y - v.y, t*n.z - v.z);
	else
	  reflect = new Vector3D(-t*n.x - v.x, -t*n.y - v.y, -t*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, depth+1);
	  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();
	}
	//}
      }

      // Compute illumination due to refraction
      if (kt > 0) {
	float ratio = (inside ? 1f/nt : nt);
	cos_ang_in = Math.abs(cos_ang_in);
	float cos_ang_ref = ratio*ratio*(1-cos_ang_in*cos_ang_in);
	if (cos_ang_ref < 1) {
	  // ang_ref is defined, so we continue to refract
	  cos_ang_ref = (float) Math.sqrt(1-cos_ang_ref);
	  float coeff = -cos_ang_ref + ratio*cos_ang_in;
	  Vector3D refract;
	  // if we're inside, the normal should point the other way ...
	  if (inside) refract = new Vector3D(-ratio*v.x - coeff*n.x,
					     -ratio*v.y - coeff*n.y,
					     -ratio*v.z - coeff*n.z);
	  else refract = new Vector3D(-ratio*v.x + coeff*n.x,
				      -ratio*v.y + coeff*n.y,
				      -ratio*v.z + coeff*n.z);
	  Vector3D poffset2 = new Vector3D(p.x+TINY*refract.x, p.y+TINY*refract.y, p.z+TINY*refract.z);
	  Ray refractedRay = new Ray(poffset2, refract);
	  if (refractedRay.trace(objects)) {
	    Color refrColor = refractedRay.Shade(lights, objects, bgnd, depth+1);
	    r += kt*refrColor.getRed()*I255;
	    g += kt*refrColor.getGreen()*I255;
	    b += kt*refrColor.getBlue()*I255;
	  } else {
	    r += kt*bgnd.getRed()*I255;
	    g += kt*bgnd.getGreen()*I255;
	    b += kt*bgnd.getBlue()*I255;
	  }
	}
      }
    }

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