/** Object for drawing a view of the scene onto pixels. */
public class Renderer {
	// destination specification
	private boolean pixelsKnown = false;
	private int[] pixels;
	private int width;
	private int height;

	// frustum specification
	private boolean frustumKnown = false;
	private double x0; // left
	private double x1; // right
	private double y0; // top
	private double y1; // bottom
	private double z0; // near
	private double z1; // far

	// view specification
	private boolean viewKnown = false;
	private Point  viewLocation;
	private Vector viewForward;
	private Vector viewUp;
	private Vector viewRight;

	/** Constructs an empty renderer. Note that the pixels array,
	 * the frustum, and the view location should all be set
	 * before this becomes useful. */
	public Renderer() { }

	/** Sets the destination array where pixels should be
	 * stored once computed. */
	public void setPixels(int[] pixels, int width, int height) {
		pixelsKnown = true;
		this.pixels = pixels;
		this.width = width;
		this.height = height;
	}

	/** Sets the coordinates of the view frustum. */
	public void setFrustum(double left, double right,
			double top, double bottom, double near, double far) {
		frustumKnown = true;
		x0 = left; x1 = right;
		y0 = top;  y1 = bottom;
		z0 = near; z1 = far;
	}

	/** Sets the location and orientation of the viewer. */
	public void setView(Point lookFrom, Point lookAt, Vector up) {
		viewKnown = true;
		viewLocation = lookFrom;
		viewForward = lookAt.subtract(lookFrom).normalize();
		viewRight = viewForward.cross(up).normalize();
		viewUp = viewRight.cross(viewForward);
	}

	/** Renders the scene into the current destination array. */
	public void render(Scene scene) {
		// ensure that all information is already specified
		if(!pixelsKnown) throw new RuntimeException("Renderer destination unknown");
		if(!frustumKnown) throw new RuntimeException("Renderer frustum unknown");
		if(!viewKnown) throw new RuntimeException("Renderer view unknown");

		// load important variables into local variables so that
		// values used within the rendering process are
		// internally consistent.
		int[] pixels = this.pixels;
		int width = this.width;
		int height = this.height;
		Point  viewLocation = this.viewLocation;
		Vector viewForward  = this.viewForward;
		Vector viewUp       = this.viewUp;
		Vector viewRight    = this.viewRight;
		double x0 = this.x0;
		double y0 = this.y0;
		double z0 = this.z0;
		double dx = (x1 - x0) / (width  - 1);
		double dy = (y1 - y0) / (height - 1);

		// trace a ray through each pixel on the screen.
		for(int row = 0; row < height; row++) {
			for(int col = 0; col < width; col++) {
				Vector direction = viewForward.scale(z0)
					.add(viewRight.scale(x0 + col * dx))
					.add(viewUp.scale(y0 + (height - 1 - row) * dy));
				Ray view = Ray.create(viewLocation, direction);
				Color color = computeRayColor(scene, view);
				if(color == null) color = Color.BLACK;
				pixels[row * width + col] = color.getRGB();
			}
		}
	}

	private Color computeRayColor(Scene scene, Ray view) {
		Intersection i = scene.traceRay(view);
		return computeColor(scene, view, i);
	}

	private Color computeColor(Scene scene, Ray view, Intersection i) {
		if(i == Intersection.NONE) return Color.BLACK;
		Point hit    = i.getHitPoint();
		Material mat = i.getMaterial();
		Vector m     = i.getNormal().normalize();
		Vector v     = view.getDirection().normalize();

		double diff = 0.0;
		double spec = 0.0;
		double specExp = mat.getSpecularExponent();
		for(Point light : scene.getLights()) {
			if(!scene.inShadow(hit, light)) { // point not in light's shadow
				Vector s = light.subtract(hit).normalize();
				double diffComponent = s.dot(m);
				if(diffComponent > 0.0) {
					Vector h = s.subtract(v).normalize();
					diff += diffComponent;
					spec += Math.max(0.0, Math.pow(h.dot(m), specExp));
				}
			}
		}
		return mat.getColor(scene.getAmbient(), diff, spec);
	}

}
