/*
Copyright (C) 2001, 2006 United States Government
as represented by the Administrator of the
National Aeronautics and Space Administration.
All Rights Reserved.
*/
package gov.nasa.worldwind.geom;

import gov.nasa.worldwind.*;

/**
 * Represents a sphere in three dimensional space.
 * <p/>
 * Instances of <code>Sphere</code> are immutable. </p>
 *
 * @author Tom Gaskins
 * @version $Id: Sphere.java 1749 2007-05-06 19:48:14Z tgaskins $
 */
public final class Sphere implements Extent, Renderable
{
    public final static Sphere UNIT_SPHERE = new Sphere(Point.ZERO, 1);
    
    private final Point center;
    private final double radius;

    /**
     * Creates a sphere that completely contains a set of points.
     *
     * @param points the <code>Point</code>s to be enclosed by the new Sphere
     * @return a <code>Sphere</code> encompassing the given array of <code>Point</code>s
     * @throws IllegalArgumentException if <code>points</code> is null or empty
     */
    public static Sphere createBoundingSphere(Point points[])
    {
        if (points == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.PointsArrayIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        if (points.length < 1)
        {
            String message = WorldWind.retrieveErrMsg("geom.Sphere.NoPointsSpecified");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        // Creates the sphere around the axis aligned bounding box of the input points.
        Point extrema[] = Point.composeExtrema(points);
        Point center = Point.midPoint(extrema[0], extrema[1]);
        double radius = 0.5 * extrema[0].distanceTo(extrema[1]);

        return new Sphere(center, radius);
    }

    /**
     * Creates a new <code>Sphere</code> from a given center and radius. <code>radius</code> must be positive (that is,
     * greater than zero), and <code>center</code> may not be null.
     *
     * @param center the center of the new sphere
     * @param radius the radius of the new sphere
     * @throws IllegalArgumentException if <code>center</code> is null or if <code>radius</code> is non-positive
     */
    public Sphere(Point center, double radius)
    {
        if (center == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.CenterIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        if (radius <= 0)
        {
            String message = WorldWind.retrieveErrMsg("geom.Sphere.RadiusIsZeroOrNegative");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        this.center = center;
        this.radius = radius;
    }

    /**
     * Obtains the radius of this <code>Sphere</code>. The radus is the distance from the center to the surface. If an
     * object's distance to this sphere's center is less than or equal to the radius, then that object is at least
     * partially within this <code>Sphere</code>.
     *
     * @return the radius of this sphere
     */
    public final double getRadius()
    {
        return this.radius;
    }

    /**
     * Obtains the diameter of this <code>Sphere</code>. The diameter is twice the radius.
     *
     * @return the diameter of this <code>Sphere</code>
     */
    public final double getDiameter()
    {
        return 2 * this.radius;
    }

    /**
     * Obtains the center of this <code>Sphere</code>.
     *
     * @return the <code>Point</code> situated at the center of this <code>Sphere</code>
     */
    public final Point getCenter()
    {
        return this.center;
    }

    /**
     * Obtains the intersections of this sphere with a line. The returned array may be either null or of zero length if
     * no intersections are discovered. It does not contain null elements and will have a size of 2 at most. Tangential
     * intersections are marked as such. <code>line</code> is considered to have infinite length in both directions.
     *
     * @param line the <code>Line</code> with which to intersect this <code>Sphere</code>
     * @return an array containing all the intersections of this <code>Sphere</code> and <code>line</code>
     * @throws IllegalArgumentException if <code>line</code> is null
     */
    public final Intersection[] intersect(Line line)
    {
        if (line == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.LineIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        double a = line.getDirection().selfDot();
        double b = 2 * line.selfDot();
        double c = line.getOrigin().selfDot() - this.radius * this.radius;

        double discriminant = Sphere.discriminant(a, b, c);
        if (discriminant < 0)
            return null;

        double discriminantRoot = Math.sqrt(discriminant);
        if (discriminant == 0)
        {
            Point p = line.getPointAt((-b - discriminantRoot) / (2 * a));
            return new Intersection[] {new Intersection(p, true)};
        }
        else // (discriminant > 0)
        {
            Point near = line.getPointAt((-b - discriminantRoot) / (2 * a));
            Point far = line.getPointAt((-b + discriminantRoot) / (2 * a));
            return new Intersection[] {new Intersection(near, false), new Intersection(far, false)};
        }
    }

    /**
     * Calculates a discriminant. A discriminant is useful to determine the number of roots to a quadratic equation. If
     * the discriminant is less than zero, there are no roots. If it equals zero, there is one root. If it is greater
     * than zero, there are two roots.
     *
     * @param a the coefficient of the second order pronumeral
     * @param b the coefficient of the first order pronumeral
     * @param c the constant parameter in the quadratic equation
     * @return the discriminant "b squared minus 4ac"
     */
    private static double discriminant(double a, double b, double c)
    {
        return b * b - 4 * a * c;
    }

    /**
     * tests for intersetion with a <code>Frustum</code>. This operation is commutative, so
     * <code>someSphere.intersects(frustum)</code> and <code>frustum.intersects(someSphere)</code> are equivalent.
     *
     * @param frustum the <code>Frustum</code> with which to test for intersection
     * @return true if either <code>frustum</code> or this <code>Sphere</code> wholly or partially contain the other,
     *         false otherwise.
     * @throws IllegalArgumentException if <code>frustum</code> is null
     */
    public boolean intersects(Frustum frustum)
    {
        if (frustum == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.FrustumIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        return frustum.intersects(this);
    }

    /**
     * Tests for intersection with a <code>Line</code>.
     *
     * @param line the <code>Line</code> with which to test for intersection
     * @return true if <code>line</code> intersects or makes a tangent with the surface of this <code>Sphere</code>
     * @throws IllegalArgumentException if <code>line</code> is null
     */
    public boolean intersects(Line line)
    {
        if (line == null)
        {
            String msg = WorldWind.retrieveErrMsg("nullValue.LineIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
            throw new IllegalArgumentException(msg);
        }
        return line.distanceTo(this.center) <= this.radius;
    }

    /**
     * Tests for intersection with a <code>Plane</code>.
     *
     * @param plane the <code>Plane</code> with which to test for intersection
     * @return true if <code>plane</code> intersects or makes a tangent with the surface of this <code>Sphere</code>
     * @throws IllegalArgumentException if <code>plane</code> is null
     */
    public boolean intersects(Plane plane)
    {
        if (plane == null)
        {
            String msg = WorldWind.retrieveErrMsg("nullValue.PlaneIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
            throw new IllegalArgumentException(msg);
        }

        double dq1 = plane.dot(this.center);
        return dq1 <= this.radius;
    }

    /**
     * Causes this <code>Sphere</code> to render itself using the <code>DrawContext</code> provided. <code>dc</code> may
     * not be null.
     *
     * @param dc the <code>DrawContext</code> to be used
     * @throws IllegalArgumentException if <code>dc</code> is null
     */
    public void render(gov.nasa.worldwind.DrawContext dc)
    {
        if (dc == null)
        {
            String msg = WorldWind.retrieveErrMsg("nullValue.DrawContextIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
            throw new IllegalArgumentException(msg);
        }

        javax.media.opengl.GL gl = dc.getGL();

        gl.glPushAttrib(javax.media.opengl.GL.GL_TEXTURE_BIT | javax.media.opengl.GL.GL_ENABLE_BIT
            | javax.media.opengl.GL.GL_CURRENT_BIT);
        gl.glDisable(javax.media.opengl.GL.GL_TEXTURE_2D);
        gl.glColor3d(1, 1, 0);

        gl.glMatrixMode(javax.media.opengl.GL.GL_MODELVIEW);
        gl.glPushMatrix();
        gl.glTranslated(this.center.x(), this.center.y(), this.center.z());
        javax.media.opengl.glu.GLUquadric quadric = dc.getGLU().gluNewQuadric();
        dc.getGLU().gluQuadricDrawStyle(quadric, javax.media.opengl.glu.GLU.GLU_LINE);
        dc.getGLU().gluSphere(quadric, this.radius, 10, 10);
        gl.glPopMatrix();
        dc.getGLU().gluDeleteQuadric(quadric);

        gl.glPopAttrib();
    }

    @Override
    public String toString()
    {
        return "Sphere: center = " + this.center.toString() + " radius = " + Double.toString(this.radius);
    }

    @Override
    public boolean equals(Object o)
    {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;

        final gov.nasa.worldwind.geom.Sphere sphere = (gov.nasa.worldwind.geom.Sphere) o;

        if (Double.compare(sphere.radius, radius) != 0)
            return false;
        if (!center.equals(sphere.center))
            return false;

        return true;
    }

    @Override
    public int hashCode()
    {
        int result;
        long temp;
        result = center.hashCode();
        temp = radius != +0.0d ? Double.doubleToLongBits(radius) : 0L;
        result = 29 * result + (int) (temp ^ (temp >>> 32));
        return result;
    }

//    public final boolean intersects(Line line)
//    {
//        if (line == null)
//        {
//            String message = WorldWind.retrieveErrMsg("nullValue.LineIsNull");
//            WorldWind.logger().log(java.util.logging.Level.FINE, message);
//            throw new IllegalArgumentException(message);
//        }
//
//        double a = line.getDirection().getLengthSquared();
//        double b = 2 * line.selfDot();
//        double c = line.getOrigin().selfDot() - this.radius * this.radius;
//
//        double discriminant = Sphere.discriminant(a, b, c);
//        if (discriminant < 0)
//        {
//            return false;
//        }
//
//        return true;
//
//    }
}
