/*
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.*;

import javax.media.opengl.*;
import javax.media.opengl.glu.*;

/**
 * Represents a geometric cylinder. <code>Cylinder</code>s are immutable.
 *
 * @author Tom Gaskins
 * @version $Id: Cylinder.java 1749 2007-05-06 19:48:14Z tgaskins $
 */
public class Cylinder implements Extent, Renderable
{
    private final Point bottomCenter; // point at center of cylinder base
    private final Point topCenter; // point at center of cylinder top
    private final Point axisUnitDirection; // axis as unit vector from bottomCenter to topCenter
    private final double cylinderRadius;
    private final double cylinderHeight;

    /**
     * Create a <code>Cylinder</code> from two points and a radius. Does not accept null arguments.
     *
     * @param bottomCenter   represents the centrepoint of the base disc of the <code>Cylinder</code>
     * @param topCenter      represents the centrepoint of the top disc of the <code>Cylinder</code>
     * @param cylinderRadius the radius of the <code>Cylinder</code>
     * @throws IllegalArgumentException if either the top or bottom point is null
     */
    public Cylinder(Point bottomCenter, Point topCenter, double cylinderRadius)
    {
        if (bottomCenter == null || topCenter == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.EndPointIsNull");

            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        if (cylinderRadius <= 0)
        {
            String message = WorldWind.retrieveErrMsg("geom.Cylinder.RadiusIsZeroOrNegative");

            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        this.bottomCenter = bottomCenter;
        this.topCenter = topCenter;
        this.cylinderHeight = this.bottomCenter.distanceTo(this.topCenter);
        this.cylinderRadius = cylinderRadius;
        this.axisUnitDirection = this.topCenter.subtract(this.bottomCenter).normalize();
    }

    public String toString()
    {
        return this.cylinderRadius + ", " + this.bottomCenter.toString() + ", " + this.topCenter.toString() + ", "
            + this.axisUnitDirection.toString();
    }

    public Intersection[] intersect(Line line) // TODO: test this method
    {
        if (line == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.LineIsNull");

            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        Point ld = line.getDirection();
        Point lo = line.getOrigin();
        double a = ld.x() * ld.x() + ld.y() * ld.y();
        double b = 2 * (lo.x() * ld.x() + lo.y() * ld.y());
        double c = lo.x() * lo.x() + lo.y() * lo.y() - this.cylinderRadius * this.cylinderRadius;

        double discriminant = Cylinder.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));
            boolean n = false, f = false;
            boolean nTangent = false, fTangent = false;
            if (near.z() >= 0 && near.z() <= this.getHeight())
            {
                n = true;
                nTangent = near.z() == 0;
            }
            if (far.z() >= 0 && far.z() <= this.getHeight())
            {
                f = true;
                fTangent = far.z() == 0;
            }

            // TODO: Test for intersection with planes at cylinder's top and bottom

            Intersection[] intersections = null;
            if (n && f)
                intersections = new Intersection[] {new Intersection(near, nTangent), new Intersection(far, fTangent)};
            else if (n)
                intersections = new Intersection[] {new Intersection(near, nTangent)};
            else if (f)
                intersections = new Intersection[] {new Intersection(far, fTangent)};

            return intersections;
        }
    }

    public 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);
        }

        Point ld = line.getDirection();
        Point lo = line.getOrigin();
        double a = ld.x() * ld.x() + ld.y() * ld.y();
        double b = 2 * (lo.x() * ld.x() + lo.y() * ld.y());
        double c = lo.x() * lo.x() + lo.y() * lo.y() - this.cylinderRadius * this.cylinderRadius;

        double discriminant = Cylinder.discriminant(a, b, c);

        return discriminant >= 0;
    }

    static private double discriminant(double a, double b, double c)
    {
        return b * b - 4 * a * c;
    }

    private double intersectsAt(Plane plane, double effectiveRadius, double parameter)
    {
        // Test the distance from the first cylinder end-point.
        double dq1 = plane.dot(this.bottomCenter);
        boolean bq1 = dq1 <= -effectiveRadius;

        // Test the distance from the possibly reduced second cylinder end-point.
        Point newTop;
        if (parameter < 1)
            newTop = this.bottomCenter.add(this.topCenter.subtract(this.bottomCenter).multiply(parameter));
        else
            newTop = this.topCenter;
        double dq2 = plane.dot(newTop);
        boolean bq2 = dq2 <= -effectiveRadius;

        if (bq1 && bq2) // both <= effective radius; cylinder is on negative side of plane
            return -1;

        if (bq1 == bq2) // both >= effective radius; can't draw any conclusions
            return parameter;

        // Compute and return the parameter value at which the plane intersects the cylinder's axis.
        return (effectiveRadius + plane.dot(this.bottomCenter))
            / plane.getNormal().dot(this.bottomCenter.subtract(newTop));
    }

    private double getEffectiveRadius(Plane plane)
    {
        // Determine the effective radius of the cylinder axis relative to the plane.
        double dot = plane.getNormal().dot(this.axisUnitDirection);
        double scale = 1d - dot * dot;
        if (scale <= 0)
            return 0;
        else
            return this.cylinderRadius * Math.sqrt(scale);
    }

    public boolean intersects(Plane plane)
    {
        if (plane == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.PlaneIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }
        double effectiveRadius = this.getEffectiveRadius(plane);
        double intersectionPoint = this.intersectsAt(plane, effectiveRadius, 1d);
        return intersectionPoint >= 0;
    }

    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);
        }

        double intersectionPoint;

        double effectiveRadius = this.getEffectiveRadius(frustum.getNear());
        intersectionPoint = this.intersectsAt(frustum.getNear(), effectiveRadius, 1d);
        if (intersectionPoint < 0)
            return false;

        // Near and far have the same effective radius.
        intersectionPoint = this.intersectsAt(frustum.getFar(), effectiveRadius, intersectionPoint);
        if (intersectionPoint < 0)
            return false;

        effectiveRadius = this.getEffectiveRadius(frustum.getLeft());
        intersectionPoint = this.intersectsAt(frustum.getLeft(), effectiveRadius, intersectionPoint);
        if (intersectionPoint < 0)
            return false;

        effectiveRadius = this.getEffectiveRadius(frustum.getRight());
        intersectionPoint = this.intersectsAt(frustum.getRight(), effectiveRadius, intersectionPoint);
        if (intersectionPoint < 0)
            return false;

        effectiveRadius = this.getEffectiveRadius(frustum.getTop());
        intersectionPoint = this.intersectsAt(frustum.getTop(), effectiveRadius, intersectionPoint);
        if (intersectionPoint < 0)
            return false;

        effectiveRadius = this.getEffectiveRadius(frustum.getBottom());
        intersectionPoint = this.intersectsAt(frustum.getBottom(), effectiveRadius, intersectionPoint);
        return intersectionPoint >= 0;
    }

    public Point getCenter()
    {
        Point b = this.bottomCenter;
        Point t = this.topCenter;
        return new Point(0.5 * (b.x() + t.x()), 0.5 * (b.y() + t.y()), 0.5 * (b.z() + t.z()));
    }

    public double getDiameter()
    {
        return 2 * this.getRadius();
    }

    public double getRadius()
    {
        // return the radius of the enclosing sphere
        double halfHeight = 0.5 * this.bottomCenter.distanceTo(this.topCenter);
        return Math.sqrt(halfHeight * halfHeight + this.cylinderRadius * this.cylinderRadius);
    }

    /**
     * Obtain the height of this <code>Cylinder</code>.
     *
     * @return the distance between the bottom and top of this <code>Cylinder</code>
     */
    public final double getHeight()
    {
        return this.cylinderHeight;
    }

    public void render(DrawContext dc)
    {
        if (dc == null)
        {
            String msg = WorldWind.retrieveErrMsg("nullValue.DrawContextIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
            throw new IllegalArgumentException(msg);
        }

        Point center = this.getCenter();
        PolarPoint p = PolarPoint.fromCartesian(center);

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

        gl.glPushAttrib(GL.GL_ENABLE_BIT | GL.GL_TRANSFORM_BIT);

        gl.glBegin(javax.media.opengl.GL.GL_LINES);
        gl.glVertex3d(this.bottomCenter.x(), this.bottomCenter.y(), this.bottomCenter.z());
        gl.glVertex3d(this.topCenter.x(), this.topCenter.y(), this.topCenter.z());
        gl.glEnd();

        gl.glEnable(javax.media.opengl.GL.GL_DEPTH_TEST);
        gl.glMatrixMode(javax.media.opengl.GL.GL_MODELVIEW);
        gl.glPushMatrix();
        gl.glTranslated(this.bottomCenter.x(), this.bottomCenter.y(), this.bottomCenter.z());
        dc.getGL().glRotated(p.getLongitude().getDegrees(), 0, 1, 0);
        dc.getGL().glRotated(Math.abs(p.getLatitude().getDegrees()), Math.signum(p.getLatitude().getDegrees()) * -1,
            0, 0);
        
        GLUquadric quadric = dc.getGLU().gluNewQuadric();
        dc.getGLU().gluQuadricDrawStyle(quadric, GLU.GLU_LINE);
        dc.getGLU().gluCylinder(quadric, this.cylinderRadius, this.cylinderRadius, this.cylinderHeight, 30, 30);
        dc.getGLU().gluDeleteQuadric(quadric);

        gl.glPopMatrix();
        gl.glPopAttrib();
    }
}
