// An example "Renderable" object
import java.awt.*;
import java.util.*;

class Sphere implements Renderable {
    Surface surface;
    Vector3D center;
    double radius;
    double radSqr;
	boolean isInside;
	boolean isBB = false;
	Vector objects;

	final static boolean DEBUG = false;

    public Sphere(Surface s, Vector3D c, double r) {
        surface = s;
        center = c;
        radius = r;
        radSqr = r*r;
		isInside = false;
    }

	// Create sphere as bounding box
    public Sphere(Triangle tri) {
        surface = new Surface(0.4, 0.4, 0.4, 0.1, 0.1, 0.6, 100.0, 0.3, .005, 1.0);
        center = new Vector3D();
		radius = getRadius(center, tri.v0, tri.v1, tri.v2);
		radSqr = radius*radius;
		isInside = false;
		if (!DEBUG)
			isBB = true;
		objects = new Vector();
		objects.addElement(tri);
    }

	// Get the radius and the center of the bounding sphere for a give triangle
	// with vertec v0, v1, and v2
  	private double getRadius(Vector3D c, Vector3D v0, Vector3D v1, Vector3D v2)
	{
		// The center I chose, it the average of the max and min of the x, y, and z values.
		// This way, the center is at the center. :)
		double xmin = getMin(v0.x, v1.x, v2.x);
		double xmax = getMax(v0.x, v1.x, v2.x);
		double ymin = getMin(v0.y, v1.y, v2.y);
		double ymax = getMax(v0.y, v1.y, v2.y);
		double zmin = getMin(v0.z, v1.z, v2.z);
		double zmax = getMax(v0.z, v1.z, v2.z);

		c.x = (xmin + xmax)/2;
		c.y = (ymin + ymax)/2;
		c.z = (zmin + zmax)/2;

		// Calculate the distance from the center to each point
		double r0 = new Vector3D(v0.x - c.x, v0.y - c.y, v0.z - c.z).length();
		double r1 = new Vector3D(v1.x - c.x, v1.y - c.y, v1.z - c.z).length();
		double r2 = new Vector3D(v2.x - c.x, v2.y - c.y, v2.z - c.z).length();
		
		// Pick the longest distance as the radius
		return getMax(r0, r1, r2);
	}

	// Get the smallest of 3 values
	private double getMin(double a, double b, double c)
	{
		double min = (a < b) ? a : b;
		return (min < c) ? min : c;
	}

	// Get the largest of 3 values
	private double getMax(double a, double b, double c)
	{
		double max = (a > b) ? a : b;
		return (max > c) ? max : c;
	}

	// Add another triangle to this group
	public void addObject(Triangle tri) {
		Vector3D newcenter = new Vector3D();
		double newradius = getRadius(newcenter, tri.v0, tri.v1, tri.v2);

		// find the distance between the center of the 2 spheres
		Vector3D dir = new Vector3D(center.x - newcenter.x, center.y - newcenter.y, center.z - newcenter.z);
		double distance = dir.length();
		dir.normalize();

		// If one sphere is inside the other, then use the larger sphere
		if (newradius + distance < radius)
		{
			objects.addElement(tri);
			return;
		}

		if (radius + distance < newradius)
		{
			radius = newradius;
			center.x = newcenter.x;
			center.y = newcenter.y;
			center.z = newcenter.z;
			radSqr = radius*radius;
			objects.addElement(tri);
			return;
		}

		// If neither sphere is inside the other, then calculate center and radius of
		//	a sphere that holds both of them

		// the diameter of the new sphere must be the sum of the radius of both spheres
		//	plus the distance between them
		double diameter = radius + newradius + distance;

		// new radius is half the diameter
		radius = diameter / 2;

		// figure out new center
		distance = radius - newradius;
		center.x = newcenter.x + distance*dir.x;
		center.y = newcenter.y + distance*dir.y;
		center.z = newcenter.z + distance*dir.z;

		radSqr = radius*radius;

		objects.addElement(tri);
	}

    public boolean intersect(Ray ray) {
        double dx = center.x - ray.origin.x;
        double dy = center.y - ray.origin.y;
        double dz = center.z - ray.origin.z;
        double v = ray.direction.dot(dx, dy, dz);

		// Initially set to false
		isInside = false;

        // 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;

		double t = 0;
		// if ray.origin in inside circle)
		if (dx*dx + dy*dy + dz*dz < radSqr)
		{
			if (!isBB)
			{
				isInside = true;
				// calculate t if ray.origin is inside
				double bSqr = dx*dx + dy*dy + dz*dz - v*v;
				t = v + Math.sqrt(radSqr - bSqr);  // note the plus as oppose to minus
				
				if (t > ray.t)
					return false;
				
				ray.t = t;
				ray.object = this;
				return true;
			}
		}
		else
		{
			// Test if the ray actually intersects the sphere
			t = radSqr + v*v - dx*dx - dy*dy - dz*dz;
			if (t < 0)
				return false;
			
			// Test if the intersection is in the positive
			// ray direction and it is the closest so far
			t = v - Math.sqrt(t);
			if ((t > ray.t) || (t < 0))
				return false;
		}

		// if sphere acts as a bounding box, then pass along to the object.
		if (isBB)
		{
			boolean bIntersect = false;
			for (Enumeration e = objects.elements(); e.hasMoreElements(); )
			{
				Renderable object = (Renderable) e.nextElement();
				if (object.intersect(ray))
					bIntersect = true;
			}
			return bIntersect;
		}

        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)
        //



        double px = ray.origin.x + ray.t*ray.direction.x;
        double py = ray.origin.y + ray.t*ray.direction.y;
        double 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();

		// if ray goes from inside out, then reverse n
		if (isInside)
		{
			n.x = -n.x;
			n.y = -n.y;
			n.z = -n.z;
		}

        // The illumination model is applied
        // by the surface's Shade() method
        return surface.Shade(p, n, v, lights, objects, bgnd, isInside, ray.factor);
    }

    public String toString() {
        return ("sphere "+center+" "+radius);
    }
}