import java.applet.*;
import java.awt.*;
import java.io.*;
import java.net.*;
import java.util.*;
import Cylinder;
import Light;
import Ray;
import Renderable;
import Sphere;
import Surface;
import Vector3D;

// SELF-NOTES TO AUTHOR:
// If time permits, end recursion when color change doesn't matter,
// in addition to a (higher) max depth.

/***************************************************
*
*   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 was contained in a single file (but is
*   now separated into multiple files). 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.
*
****************************************************
*
*   Revisions to the code by Jonathan Lie, 12/1998
*     Surface.java:
*      - Reflection inside objects
*      - Refraction
*      - Limit on amount of recursion
*        (to prevent stack overflow)
*     Cylinder.java:
*      - Original implementation of this renderable object
*     General:
*      - Minor changes here and there to compensate
*        for the modifications mentioned above
*
****************************************************/


//    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+": "+e.getMessage());
      System.err.println("Error reading "+filename+": "+e.getMessage());
      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));

	} else if (st.sval.equals("cylinder")) {
	  Vector3D ctr = new Vector3D((float) getNumber(st), (float) getNumber(st), (float) getNumber(st));
	  Vector3D ax = new Vector3D((float) getNumber(st), (float) getNumber(st), (float) getNumber(st));
	  float h = (float) getNumber(st);
	  float r = (float) getNumber(st);
	  objectList.addElement(new Cylinder(currentSurface, ctr, ax, h, r));

	} 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());
  }
  
  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 (ray.trace(objectList)) {
      gc.setColor(ray.Shade(lightList, objectList, background, 0));
    } else {
      gc.setColor(background);
    }
    // instead of using a raster, we ...
    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;
  }

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