import java.util.*;
import java.io.*;

/** Represents the static objects in the world. Essentially,
 * this is simply a set of surfaces. */
public class World {
	private Collection<Surface> surfs;
	private Bounds bounds;

	/** Constructs a world containing the given surfaces.
	 *
	 * @param surfaces contains the surfaces to be added into
	 * this world.
	 */
	public World(Collection<Surface> surfaces) {
		ArrayList<Surface> surfsBase = new ArrayList<Surface>();
		surfsBase.addAll(surfaces);
		surfs = Collections.unmodifiableCollection(surfsBase);

		bounds = Bounds.EMPTY_BOUNDS;
		for(Surface s : surfs) bounds = bounds.add(s.getBounds());
	}

	/** Returns information about the intersection of the given
	 * ray with the closest surface that it intersects, or
	 * <code>null</code> if there is no intersection.
	 *
	 * @param query the ray that should be traced to determine
	 * which surface it intersects first.
	 *
	 * @return an Intersection describing information about the
	 * first intersection, or <code>null</code> if there is no
	 * intersection.
	 */
	public Intersection getClosestIntersection(Ray query) {
		double ret = Double.POSITIVE_INFINITY;
		Surface rets = null;
		for(Surface surf : surfs) {
			double d = surf.getDistanceFrom(query);
			if(d < ret) {
				ret = d;
				rets = surf;
			}
		}
		if(ret == Double.POSITIVE_INFINITY) return null;
		double hitx = query.getX() + ret * query.getDeltaX();
		double hity = query.getY() + ret * query.getDeltaY();
		return new Intersection(ret, rets, rets.getNormal(hitx, hity));
	}

	/** Returns a collection including all the surfaces of this
	 * world. The returned collection should be unmodifiable.
	 *
	 * @return the unmodifiable collection including all
	 * surfaces in this world.
	 */
	public Collection<Surface> getSurfaces() {
		return surfs;
	}

	/** Returns the bounding box including all surfaces in this
	 * world.
	 * @return the bounding box of the surfaces' union. */
	public Bounds getBounds() {
		return bounds;
	}

	/** Returns a newly created world based on the data found in
	 * the given stream. */
	public static World read(InputStream in) throws IOException {
		return read(new InputStreamReader(in));
	}

	/** Returns a newly created world based on the data found in
	 * the given input source. */
	public static World read(Reader reader) throws IOException {
		ArrayList<Surface> surfs = new ArrayList<Surface>();
		BufferedReader in = new BufferedReader(reader);
		int line_num = 0;
		while(true) {
			++line_num;
			String line = in.readLine();
			if(line == null) break;
			StringTokenizer toks = new StringTokenizer(line, "\t");
			if(!toks.hasMoreTokens()) continue;

			String typeStr = toks.nextToken();
			int type; // 0 vert; 1 horz; 2 cyl
			if(typeStr.equals("v")) type = 0;
			else if(typeStr.equals("h")) type = 1;
			else if(typeStr.equals("c")) type = 2;
			else throw new IOException(line_num + ": bad type");

			if(toks.countTokens() < 4) {
				throw new IOException(line_num + ": missing tokens");
			} else if(toks.countTokens() > 4) {
				throw new IOException(line_num + ": extra tokens");
			}

			double x;
			double y0;
			double y1;
			try {
				x = Double.parseDouble(toks.nextToken());
				y0 = Double.parseDouble(toks.nextToken());
				y1 = Double.parseDouble(toks.nextToken());
			} catch(NumberFormatException e) {
				throw new IOException(line_num + ": bad number");
			}

			Texture txt = Texture.load(toks.nextToken());

			switch(type) {
			case 0: surfs.add(new LongWall(x, y0, y1, txt)); break;
			case 1: surfs.add(new LatWall(x, y0, y1, txt)); break;
			case 2: surfs.add(new Cylinder(x, y0, y1, txt)); break;
			}
		}
		return new World(surfs);
	}
}
