import java.applet.*;
import java.awt.*;
import java.io.*;
import java.net.*;
import java.util.*;

/***************************************************
*
*   An instructional Ray-Tracing Renderer written
*   for MIT 6.837  Fall '98 by Leonard McMillan.
*
*   A fairly primitive Ray-Tracing program written
*   on a Sunday afternoon before Monday's class.
*   Everything is contained in a single file. The
*   structure should be fairly easy to extend, with
*   new primitives, features and other such stuff.
*
*   I tend to write things bottom up (old K&R C
*   habits die slowly). If you want the big picture
*   scroll to the applet code at the end and work
*   your way back here.
*
****************************************************/

// A simple vector class 
class Vector3D { public float x, y, z;
  
  // constructors
  public Vector3D( ) {
  }

  public Vector3D(float x, float y, float z) {
    this.x = x; this.y = y; this.z = z;
  }

  public Vector3D(Vector3D v) {
    x = v.x;
    y = v.y;
    z = v.z;
  }

  // methods
  public final void scale (float k) {
    x*=k;   y*=k;   z*=k;
  }

  public final Vector3D plus(Vector3D B) {
    return new Vector3D (x+B.x, y+B.y, z+B.z);
  }

  public final Vector3D minus(Vector3D B) {
    return new Vector3D (x-B.x, y-B.y, z-B.z);
  }

  public final float dot(Vector3D B) {
    return (x*B.x + y*B.y + z*B.z);
  }

  public final float dot(float Bx, float By, float Bz) {
    return (x*Bx + y*By + z*Bz);
  }

  public static final float dot(Vector3D A, Vector3D B) {
    return (A.x*B.x + A.y*B.y + A.z*B.z);
  }

  public final Vector3D cross(Vector3D B) {
    return new Vector3D(y*B.z - z*B.y, z*B.x - x*B.z, x*B.y - y*B.x);
  }

  public final Vector3D cross(float Bx, float By, float Bz) {
    return new Vector3D(y*Bz - z*By, z*Bx - x*Bz, x*By - y*Bx);
  }

  public final static Vector3D cross(Vector3D A, Vector3D B) {
    return new Vector3D(A.y*B.z - A.z*B.y, A.z*B.x - A.x*B.z, A.x*B.y - A.y*B.x);
  }

  public final float length( ) {
    return (float) Math.sqrt(x*x + y*y + z*z);
  }

  public final static float length(Vector3D A) {
    return (float) Math.sqrt(A.x*A.x + A.y*A.y + A.z*A.z);
  }

  public final void normalize( ) {
    float t = x*x + y*y + z*z;
    if (t != 0 && t != 1) t = (float) (1 / Math.sqrt(t));
    x *= t;
    y *= t;
    z *= t;
  }

  public final static Vector3D normalize(Vector3D A) {
    float t = A.x*A.x + A.y*A.y + A.z*A.z;
    if (t != 0 && t != 1) t = (float)(1 / Math.sqrt(t));
    return new Vector3D(A.x*t, A.y*t, A.z*t);
  }

  public String toString() {
    return new String("["+x+", "+y+", "+z+"]");
  }
}

/************************************************************************/
class Ray {
  public static final float MAX_T = Float.MAX_VALUE;
  Vector3D origin;
  Vector3D direction;
  float t;
  Renderable object;  //the first object intersected by the ray
  boolean inside;  //is the ray currently inside an object?
  public static int bounces; // the number of bounces for the current pixel
  int intersectface;  // kluge allows accessing of face normal for object shading

  public Ray(Vector3D eye, Vector3D dir) {
    origin = new Vector3D(eye);
    direction = Vector3D.normalize(dir);
    inside= false;
  }
  
  public Ray(Ray copy) {
    origin= new Vector3D(copy.origin);
    direction= new Vector3D(copy.direction);
    t=copy.t;
    //object= new Renderable(copy.object);
    inside= copy.inside;
    intersectface= copy.intersectface;
  }

  public boolean trace(Vector objects) {
    Enumeration objList = objects.elements();
    t = MAX_T;
    object = null;
    
    while (objList.hasMoreElements()) {
      Renderable object = (Renderable) objList.nextElement();
      object.intersect(this);
    }
    return (object != null);
  }

  // The following method is not strictly needed, and most likely
  // adds unnecessary overhead, but I prefered the syntax
  //
  //            ray.Shade(...)
  // to
  //            ray.object.Shade(ray, ...)
  //
  public final Color Shade(Vector lights, Vector objects, Color bgnd) {
    return object.Shade(this, lights, objects, bgnd);
  }

  public String toString() {
    return ("ray origin = "+origin+"  direction = "+direction+"  t = "+t);
  }
}

/************************************************************************/
// All the public variables here are ugly, but I
// wanted Lights and Surfaces to be "friends"
class Light {
  public static final int AMBIENT = 0;
  public static final int DIRECTIONAL = 1;
  public static final int POINT = 2;

  public int lightType;
  public Vector3D lvec;           // the position of a point light or
  // the direction to a directional light
  public float ir, ig, ib;        // intensity of the light source

  public Light(int type, Vector3D v, float r, float g, float b) {
    lightType = type;
    ir = r;
    ig = g;
    ib = b;
    if (type != AMBIENT) {
      lvec = v;
      if (type == DIRECTIONAL) {
	lvec.normalize();
      }
    }
  }
}

/************************************************************************/
class Surface {
  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; 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;

    /* Do all the shading for light sources */
    if (!inside)
      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);

	  ////if (RayTrace.finished) 
	    //System.out.println("*****About to trace shadow");
	  Ray shadowRay = new Ray(poffset, l);
	  if (shadowRay.trace(objects))
	    break;
	  ////if (RayTrace.finished) 
	    //System.out.println("*****Traced shadow");

	  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
    if ((kr > 0) && (!inside)) {  //don't do interior reflections - they're infinite!
      float t = v.dot(n);
      if (t > 0) {
	t *= 2;
	Vector3D 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);
	
	/*RPS added bouncing counter to avoid stack overflow*/
	Ray.bounces++;
	//if (RayTrace.finished) {
	  //System.out.println("Point ("+p.x+","+p.y+","+p.z+")");
	  //System.out.println("  reflecting from "+(inside? "in":"out")+
	  //			     " to "+(inside? "in":"out")+
	  //	     " ...bounces = "+Ray.bounces);
	//}
	if (Ray.bounces > 8) {
	  Ray.bounces--;
	  r = (r > 1f) ? 1f : r;
	  g = (g > 1f) ? 1f : g;
	  b = (b > 1f) ? 1f : b;
	  return new Color(r, g, b);
	}
	
	if (reflectedRay.trace(objects)) {
	  Color rcolor = reflectedRay.Shade(lights, objects, bgnd);
	  r += kr*rcolor.getRed()*I255;
	  g += kr*rcolor.getGreen()*I255;
	  b += kr*rcolor.getBlue()*I255;
	} else {
	  r += kr*bgnd.getRed()*I255;
	  g += kr*bgnd.getGreen()*I255;
	  b += kr*bgnd.getBlue()*I255;
	}
      }
    }

    /******** MY CODE ********/
    // Compute illumination due to REFRACTION
    if (kt > 0) {  //if any light is refracted

      /* set the refraction index according to whether we're inside or outside */
      float nT;  //the refraction index accounting for inside/outside
      if (inside)  nT= 1f/nt;
      else  nT= nt;
	 
      /* Figure out the transmission vector using Snell's Law */
      float costhetaI= n.dot(v);
      float costhetaR= (float) Math.sqrt( 1f - nT*nT*( 1f - costhetaI*costhetaI));
      float k= costhetaR - nt*costhetaI; //a constant to save on computation
      float tx= -nT*v.x - k*n.x;
      float ty= -nT*v.y - k*n.y;
      float tz= -nT*v.z - k*n.z;
      Vector3D transmission= new Vector3D(tx, ty, tz);
      transmission.normalize();

      /* Compute the starting point and the transmission ray */
      Vector3D poffset = new Vector3D(p.x + TINY*transmission.x, 
				      p.y + TINY*transmission.y, 
				      p.z + TINY*transmission.z);
      Ray transmissionRay= new Ray(poffset, transmission);
      transmissionRay.inside= !inside;

      /*RPS added bouncing counter to avoid stack overflow*/
      Ray.bounces++;
      //if (RayTrace.finished) {
	////System.out.print("Point ("+p.x+","+p.y+","+p.z+")");
	//System.out.println(" refracting from "+
	//			   (inside? "in":"out")+
			     //	    " to "+(transmissionRay.inside? "in":"out")+
			     //" ...bounces = "+Ray.bounces);
	//System.out.println("Poffset ("+poffset.x+","+poffset.y+","+poffset.z+")");
	//System.out.println("T ("+transmissionRay.direction.x+","+
      //		   transmissionRay.direction.y+","+
      //	   transmissionRay.direction.z+")");
      //}
      if (Ray.bounces > 8) {
	Ray.bounces--;  //we pop back up to the next level of the tree and continue
	r = (r > 1f) ? 1f : r;
	g = (g > 1f) ? 1f : g;
	b = (b > 1f) ? 1f : b;
	return new Color(r, g, b);
      }

      /* Trace the refraction */
      if (transmissionRay.trace(objects)) {
	Color tcolor = transmissionRay.Shade(lights, objects, bgnd);
	//if (RayTrace.finished) {
	  //System.out.println("Refraction returns color = "+tcolor.toString());
	  //System.out.println("kt= "+kt);
	//}
	r += kt*tcolor.getRed()*I255;
	g += kt*tcolor.getGreen()*I255;
	b += kt*tcolor.getBlue()*I255;
      } else {
	r += kt*bgnd.getRed()*I255;
	g += kt*bgnd.getGreen()*I255;
	b += kt*bgnd.getBlue()*I255;
      }
    }
    /******** END OF MY CODE ********/

    r = (r > 1f) ? 1f : r;
    g = (g > 1f) ? 1f : g;
    b = (b > 1f) ? 1f : b;	
    //    //if (RayTrace.finished) 
    ////System.out.println("therefore, surface color = "+surfacecolor.toString());

    return new Color(r, g, b);
  }
}

/************************************************************************/
// An object must implement a Renderable interface in order to
// be ray traced. Using this interface it is straight forward
// to add new objects
abstract interface Renderable {
  public abstract boolean intersect(Ray r);
  public abstract Color Shade(Ray r, Vector lights, Vector objects, Color bgnd);
  public String toString();
}

/************************************************************************/
// An example "Renderable" object
class Sphere implements Renderable {
  Surface surface;
  Vector3D center;
  float radius;
  float radSqr;

  public Sphere(Surface s, Vector3D c, float r) {
    surface = s;
    center = c;
    radius = r;
    radSqr = r*r;
  }

  public boolean intersect(Ray ray) {
    float dx = center.x - ray.origin.x;
    float dy = center.y - ray.origin.y;
    float dz = center.z - ray.origin.z;
    float v = ray.direction.dot(dx, dy, dz);


    // Do the following quick check to see if there is even a chance
    // that an intersection here might be closer than a previous one
    if (v - radius > ray.t) {
      return false;
    }

    // Test if the ray actually intersects the sphere
    float t = radSqr + v*v - dx*dx - dy*dy - dz*dz;
    if (t < 0) {
      return false;
    }

    //added by RPS to allow for inside refraction
    if (ray.inside)
      t = v + ((float) Math.sqrt(t));
    else //outside
      t = v - ((float) Math.sqrt(t));

    // Test if the intersection is in the positive
    // ray direction    
    if (t < 0) {
      return false;
    }
    
    // Test if it is the closest so far
    if (t > ray.t) {
      return false;
    }

    ray.t = t;
    ray.object = this;
    return true;
  }

  public Color Shade(Ray ray, Vector lights, Vector objects, Color bgnd) {
    // An object shader doesn't really do too much other than
    // supply a few critical bits of geometric information
    // for a surface shader. It must must compute:
    //
    //   1. the point of intersection (p)
    //   2. a unit-length surface normal (n)
    //   3. a unit-length vector towards the ray's origin (v)
    //
    float px = ray.origin.x + ray.t*ray.direction.x;
    float py = ray.origin.y + ray.t*ray.direction.y;
    float pz = ray.origin.z + ray.t*ray.direction.z;

    Vector3D p = new Vector3D(px, py, pz);
    Vector3D v = new Vector3D(-ray.direction.x, -ray.direction.y, -ray.direction.z);
    Vector3D n = new Vector3D(px - center.x, py - center.y, pz - center.z);
    n.normalize();

    // The illumination model is applied
    // by the surface's Shade() method
    Color c= surface.Shade(p, n, v, lights, objects, bgnd, ray.inside);
    
    //if (RayTrace.finished) 
      //System.out.println("Shaded sphere w/ center "+center.toString()+
      //			 "to color = "+c.toString());
    return c;
  }

  public String toString() {
    return ("sphere "+center+" "+radius);
  }
}

/************************************************************************/
//My Renderable object
class TriPrism implements Renderable {
  Surface surface;
  Vector3D vertices[];
  //plane euations consist of normals and offsets
  // Ax + By + Cz + D = 0
  Vector3D normals[];
  float d[];
  //Save a bounding sphere for trivial rejection
  Sphere boundingSphere;
  
  /**********************************************************************/
  public TriPrism (Vector3D v0, Vector3D v1, Vector3D v2, Vector3D v3, Surface s) {
    //vertices given in clockwise order around the first face
    surface=s;
    vertices= new Vector3D[4];
    vertices[0]= v0;
    vertices[1]= v1;
    vertices[2]= v2;
    vertices[3]= v3;

    /*compute normals using edge cross products*/
    Vector3D edge0= new Vector3D(v1.minus(v0));
    Vector3D edge1= new Vector3D(v2.minus(v0));
    Vector3D edge2= new Vector3D(v3.minus(v0));
    Vector3D edge3= new Vector3D(v2.minus(v1));
    Vector3D edge4= new Vector3D(v3.minus(v1));
    normals= new Vector3D[4];
    normals[0]= edge1.cross(edge0);
    normals[1]= edge0.cross(edge2);
    normals[2]= edge2.cross(edge1);
    normals[3]= edge3.cross(edge4);
    normals[0].normalize();    normals[1].normalize();    
    normals[2].normalize();    normals[3].normalize();
    
    /*compute offsets using D = -Ax - By - Cz */
    d= new float[4];
    d[0]= -1 * normals[0].dot(v0);
    d[1]= -1 * normals[1].dot(v0);
    d[2]= -1 * normals[2].dot(v0);
    d[3]= -1 * normals[3].dot(v1);

    /*compute the bounding sphere, useful for trivial rejection*/
    //make the center the average of the 4 vertices
    Vector3D center= new Vector3D(v0.x, v0.y, v0.z);
    for (int i=1; i<4; i++) 
      center= center.plus(vertices[i]);
    center.scale(0.25f);  
    //make the radius the max distance from the center to any of the vertices*/
    float radius=0;
    for (int i=0; i<4; i++) {
      float distance= (vertices[i].minus(center)).length();
      if (distance > radius)
	radius= distance;
    }
    boundingSphere= new Sphere(s, center, radius);
    
    //    //System.out.println();
    //    //System.out.println(toString());
  }
  
  /**********************************************************************/
  public boolean intersect(Ray ray) {
    float test;

    Ray sphereRay= new Ray(ray);
    if (!boundingSphere.intersect(sphereRay))
      return false;

    for (int face=0; face<4; face++) {
      //test if it's facing the right way
      float axbycz= - ray.direction.dot(normals[face]);
      if (ray.inside) test= -axbycz;
      else /*outside*/ test= axbycz;
      if (test > 0) {
	//s is the distance to the face
	float s= (d[face] + normals[face].dot(ray.origin)) / axbycz;
	Vector3D raydelta= new Vector3D(ray.direction);
	raydelta.scale(s);
	Vector3D intersection= new Vector3D(ray.origin.plus(raydelta));
	if (insideEdges(face, intersection)) {
	  ray.t= s;
	  ray.object= this;
	  ray.intersectface= face;  //store the intersect face for later use
	  //if (RayTrace.finished) 
	    //System.out.println("intersect face = "+ray.intersectface+
	    //		       " intersection= "+intersection.toString());
	  return true;  //break out of the for loop, we've found the intersection
	}
      }
    } //end for
    
    return false;  //if it didn't hit any of the faces correctly, no intersection
  }//end intersect

  /**********************************************************************/
  public boolean insideEdges (int face, Vector3D intersection) {
    /* Intersection is inside each of the edges of this face if plane
     * equations for all the other faces are negative (or zero, if it's on the
     * edge).
     */
    boolean inside;
    for (int f=0; f<4; f++) {
      if (f!=face) {
	inside= ((intersection.dot(normals[f]) + d[f]) <= 0);
	if (!inside) return false;
      }
    }
    return true;
  }

  /**********************************************************************/
  public Color Shade(Ray ray, Vector lights, Vector objects, Color bgnd) {
    // An object shader doesn't really do too much other than
    // supply a few critical bits of geometric information
    // for a surface shader. It must must compute:
    //
    //   1. the point of intersection (p)
    //   2. a unit-length surface normal (n)
    //   3. a unit-length vector towards the ray's origin (v)
    //
    float px = ray.origin.x + ray.t*ray.direction.x;
    float py = ray.origin.y + ray.t*ray.direction.y;
    float pz = ray.origin.z + ray.t*ray.direction.z;
    Vector3D p = new Vector3D(px, py, pz);
    Vector3D v = new Vector3D(-ray.direction.x, -ray.direction.y, -ray.direction.z);
    Vector3D n = normals[ray.intersectface];
    //if (RayTrace.finished) 
      //System.out.println("intersect face = "+ray.intersectface);

    // The illumination model is applied
    // by the surface's Shade() method
    return surface.Shade(p, n, v, lights, objects, bgnd, ray.inside);
  }

  public String toString() {
    return ("triprism ==> v0= "+vertices[0].toString()+"\n"+
	    "===========> v1= "+vertices[1].toString()+"\n"+
	    "===========> v2= "+vertices[2].toString()+"\n"+
	    "===========> v3= "+vertices[3].toString()+"\n"+
	    "xxxxxxxxxxx> n0= "+normals[0].toString()+"\n"+
	    "xxxxxxxxxxx> n1= "+normals[1].toString()+"\n"+
	    "xxxxxxxxxxx> n2= "+normals[2].toString()+"\n"+
	    "xxxxxxxxxxx> n3= "+normals[3].toString()+"\n"+
	    "Bounding Sphere= "+boundingSphere.toString());
  }
}

/************************************************************************/
//    The following Applet demonstrates a simple ray tracer with a
//    mouse-based painting interface for the impatient and Mac owners
public class RayTrace extends Applet implements Runnable {
  final static int CHUNKSIZE = 100;
  Image screen;
  Graphics gc;
  Vector objectList;
  Vector lightList;
  Surface currentSurface;

  Vector3D eye, lookat, up;
  Vector3D Du, Dv, Vp;
  float fov;

  Color background;

  int width, height;

  public void init( ) {
    // initialize the off-screen rendering buffer
    width = size().width;
    height = size().height;
    screen = createImage(width, height);
    gc = screen.getGraphics();
    gc.setColor(getBackground());
    gc.fillRect(0, 0, width, height);

    fov = 30;               // default horizonal field of view

    // Initialize various lists
    objectList = new Vector(CHUNKSIZE, CHUNKSIZE);
    lightList = new Vector(CHUNKSIZE, CHUNKSIZE);
    currentSurface = new Surface(0.8f, 0.2f, 0.9f, 0.2f, 0.4f, 0.4f, 10.0f, 0f, 0f, 1f);

    // Parse the scene file
    String filename = getParameter("datafile");
    showStatus("Parsing " + filename);
    InputStream is = null;
    try {
      is = new URL(getDocumentBase(), filename).openStream();
      ReadInput(is);
      is.close();
    } catch (IOException e) {
      showStatus("Error reading "+filename);
      System.err.println("Error reading "+filename);
      System.exit(-1);
    }

    // Initialize more defaults if they weren't specified
    if (eye == null) eye = new Vector3D(0, 0, 10);
    if (lookat == null) lookat = new Vector3D(0, 0, 0);
    if (up  == null) up = new Vector3D(0, 1, 0);
    if (background == null) background = new Color(0,0,0);

    // Compute viewing matrix that maps a
    // screen coordinate to a ray direction
    Vector3D look = new Vector3D(lookat.x - eye.x, lookat.y - eye.y, lookat.z - eye.z);
    Du = Vector3D.normalize(look.cross(up));
    Dv = Vector3D.normalize(look.cross(Du));
    float fl = (float)(width / (2*Math.tan((0.5*fov)*Math.PI/180)));
    Vp = Vector3D.normalize(look);
    Vp.x = Vp.x*fl - 0.5f*(width*Du.x + height*Dv.x);
    Vp.y = Vp.y*fl - 0.5f*(width*Du.y + height*Dv.y);
    Vp.z = Vp.z*fl - 0.5f*(width*Du.z + height*Dv.z);
  }


  double getNumber(StreamTokenizer st) throws IOException {
    if (st.nextToken() != StreamTokenizer.TT_NUMBER) {
      System.err.println("ERROR: number expected in line "+st.lineno());
      throw new IOException(st.toString());
    }
    return st.nval;
  }

  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("sphere")) {
	Vector3D v = new Vector3D((float) getNumber(st), (float) getNumber(st), (float) getNumber(st));
	float r = (float) getNumber(st);
	objectList.addElement(new Sphere(currentSurface, v, r));
      } 
      /*RPS added- to allow for triprism*/
      else if (st.sval.equals("triprism")) {
	Vector3D v0 = new Vector3D((float) getNumber(st), (float) getNumber(st), 
				   (float) getNumber(st));
	Vector3D v1 = new Vector3D((float) getNumber(st), (float) getNumber(st), 
				   (float) getNumber(st));
	Vector3D v2 = new Vector3D((float) getNumber(st), (float) getNumber(st), 
				   (float) getNumber(st));
	Vector3D v3 = new Vector3D((float) getNumber(st), (float) getNumber(st), 
				   (float) getNumber(st));
	objectList.addElement(new TriPrism(v0,v1,v2,v3,currentSurface));
      } else
	if (st.sval.equals("eye")) {
	  eye = new Vector3D((float) getNumber(st), (float) getNumber(st), (float) getNumber(st));
	} else
	  if (st.sval.equals("lookat")) {
	    lookat = new Vector3D((float) getNumber(st), (float) getNumber(st), (float) getNumber(st));
	  } else
	    if (st.sval.equals("up")) {
	      up = new Vector3D((float) getNumber(st), (float) getNumber(st), (float) getNumber(st));
	    } else
	      if (st.sval.equals("fov")) {
		fov = (float) getNumber(st);
	      } else
		if (st.sval.equals("background")) {
		  background = new Color((float) getNumber(st), (float) getNumber(st), (float) getNumber(st));
		} else
		  if (st.sval.equals("light")) {
		    float r = (float) getNumber(st);
		    float g = (float) getNumber(st);
		    float b = (float) getNumber(st);
		    if (st.nextToken() != StreamTokenizer.TT_WORD) {
		      throw new IOException(st.toString());
                    }
		    if (st.sval.equals("ambient")) {
		      lightList.addElement(new Light(Light.AMBIENT, null, r, g, b));
		    } else
		      if (st.sval.equals("directional")) {
			Vector3D v = new Vector3D((float) getNumber(st), (float) getNumber(st), (float) getNumber(st));
			lightList.addElement(new Light(Light.DIRECTIONAL, v, r, g, b));
		      } else
			if (st.sval.equals("point")) {
			  Vector3D v = new Vector3D((float) getNumber(st), (float) getNumber(st), (float) getNumber(st));
			  lightList.addElement(new Light(Light.POINT, v, r, g, b));
			} else {
			  System.err.println("ERROR: in line "+st.lineno()+" at "+st.sval);
			  throw new IOException(st.toString());
			}
		  } else
		    if (st.sval.equals("surface")) {
		      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);
		      float kr = (float) getNumber(st);
		      float kt = (float) getNumber(st);
		      float index = (float) getNumber(st);
		      currentSurface = new Surface(r, g, b, ka, kd, ks, ns, kr, kt, index);
		    }
      break;
    }
  }
  is.close();
  if (st.ttype != StreamTokenizer.TT_EOF)
    throw new IOException(st.toString());
  }
    
  //make finished boolean and static to allow testing within all the different
  //classes
  public static boolean finished = false;
    
  public void paint(Graphics g) {
    if (finished)
      g.drawImage(screen, 0, 0, this);
  }

  // this overide avoid the unnecessary clear on each paint()
  public void update(Graphics g) {
    paint(g);
  }


  Thread raytracer;

  public void start() {
    if (raytracer == null) {
      raytracer = new Thread(this);
      raytracer.start();
    } else {
      raytracer.resume();
    }
  }

  public void stop() {
    if (raytracer != null) {
      raytracer.suspend();
    }
  }

  private void renderPixel(int i, int j) {
    Vector3D dir = new Vector3D(
                                i*Du.x + j*Dv.x + Vp.x,
                                i*Du.y + j*Dv.y + Vp.y,
                                i*Du.z + j*Dv.z + Vp.z);
    Ray ray = new Ray(eye, dir);

    //if (RayTrace.finished)
      //System.out.println("**********************rendering pixel ("+i+", "+j+")");

    Ray.bounces=0;
    if (ray.trace(objectList)) {
      gc.setColor(ray.Shade(lightList, objectList, background));
    } else {
      gc.setColor(background);
    }
    gc.drawLine(i, j, i, j);        // oh well, it works.
  }

  public void run() {
    Graphics g = getGraphics();
    long time = System.currentTimeMillis();
    for (int j = 0; j < height; j++) {
      showStatus("Scanline "+j);
      for (int i = 0; i < width; i++) {
 	renderPixel(i, j);
      }
      g.drawImage(screen, 0, 0, this); //doing this less often speed things up a bit
    } 
    g.drawImage(screen, 0, 0, this); 
    time = System.currentTimeMillis() - time;
    showStatus("Rendered in "+(time/60000)+":"+((time%60000)*0.001));
    finished = true;
  }


  public boolean mouseDown(Event e, int x, int y) {
    renderPixel(x, y);
    repaint();
    return true;
  }

  public boolean mouseDrag(Event e, int x, int y) {
    renderPixel(x, y);
    repaint();
    return true;
  }
}
