/*
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 java.util.logging.Level;

/**
 * @author Paul Collins
 * @version $Id: ViewFrustum.java 1774 2007-05-08 01:03:37Z dcollins $
 */
public class ViewFrustum
{
    private final Frustum frustum;
    private final Matrix4 projection;
//    private java.awt.geom.Rectangle2D nearRect;
//    private java.awt.geom.Rectangle2D farRect;

    public ViewFrustum(Matrix4 projectionMatrix)
    {
        if (projectionMatrix == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.MatrixIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        double[] m = projectionMatrix.getEntries();
        // Extract the near clipping plane from the projection-matrix.
        double nearMag = Math.sqrt((m[3] + m[2]) * (m[3] + m[2]) + (m[7] + m[6]) * (m[7] + m[6])
            + (m[11] + m[10]) * (m[11] + m[10]));
        Plane nearPlane = new Plane((m[3] + m[2]) / nearMag, (m[7] + m[6]) / nearMag, (m[11] + m[10]) / nearMag,
            m[15] + m[14]);
        // Extract the far clipping plane from the projection-matrix.
        double farMag = Math.sqrt((m[3] - m[2]) * (m[3] - m[2]) + (m[7] - m[6]) * (m[7] - m[6])
            + (m[11] - m[10]) * (m[11] - m[10]));
        Plane farPlane = new Plane((m[3] - m[2]) / farMag, (m[7] - m[6]) / farMag, (m[11] - m[10]) / farMag,
            m[15] - m[14]);
        // Extract the left clipping plane from the projection-matrix.
        double leftMag = Math.sqrt((m[3] + m[0]) * (m[3] + m[0]) + (m[7] + m[4]) * (m[7] + m[4])
            + (m[11] + m[8]) * (m[11] + m[8]));
        Plane leftPlane = new Plane((m[3] + m[0]) / leftMag, (m[7] + m[4]) / leftMag, (m[11] + m[8]) / leftMag,
            m[15] + m[12]);
        // Extract the right clipping plane from the projection-matrix.
        double rightMag = Math.sqrt((m[3] - m[0]) * (m[3] - m[0]) + (m[7] - m[4]) * (m[7] - m[4])
            + (m[11] - m[8]) * (m[11] - m[8]));
        Plane rightPlane = new Plane((m[3] - m[0]) / rightMag, (m[7] - m[4]) / rightMag, (m[11] - m[8]) / rightMag,
            m[15] - m[12]);
        // Extract the bottom clipping plane from the projection-matrix.
        double bottomMag = Math.sqrt((m[3] + m[1]) * (m[3] + m[1]) + (m[7] + m[5]) * (m[7] + m[5])
            + (m[11] + m[9]) * (m[11] + m[9]));
        Plane bottomPlane = new Plane((m[3] + m[1]) / bottomMag, (m[7] + m[5]) / bottomMag, (m[11] + m[9]) / bottomMag,
            m[15] + m[13]);
        // Extract the top clipping plane from the projection-matrix.
        double topMag = Math.sqrt((m[3] - m[1]) * (m[3] - m[1]) + (m[7] - m[5]) * (m[7] - m[5])
            + (m[11] - m[9]) * (m[11] - m[9]));
        Plane topPlane = new Plane((m[3] - m[1]) / topMag, (m[7] - m[5]) / topMag, (m[11] - m[9]) / topMag,
            m[15] - m[13]);
        this.frustum = new Frustum(nearPlane, farPlane, leftPlane, rightPlane, bottomPlane, topPlane);
        this.projection = projectionMatrix;
    }

    /**
     * Creates a <code>Frustum</code> from a horizontal field-of-view, viewport aspect ratio and distance to near and
     * far depth clipping planes. The near plane must be closer than the far plane, and both planes must be a positive
     * distance away.
     *
     * @param fieldOfView    horizontal field-of-view angle in the range (0, 180)
     * @param viewportWidth  the width of the viewport in screen pixels
     * @param viewportHeight the height of the viewport in screen pixels
     * @param near           distance to the near depth clipping plane
     * @param far            distance to far depth clipping plane
     * @throws IllegalArgumentException if fov is not in the range (0, 180), if either near or far are negative, or near
     *                                  is greater than or equal to far
     */
    public ViewFrustum(Angle fieldOfView, int viewportWidth, int viewportHeight, double near, double far)
    {
        if (fieldOfView == null)
        {
            String message = WorldWind.retrieveErrMsg("geom.ViewFrustum.FieldOfViewIsNull");
            WorldWind.logger().log(Level.FINE, message);
            throw new IllegalArgumentException(message);
        }
        double fov = fieldOfView.getDegrees();
        double farMinusNear = far - near;
        String message = null;
        if (fov <= 0 || fov > 180)
            message = WorldWind.retrieveErrMsg("geom.ViewFrustum.FieldOfViewOutOfRange");
        if (near <= 0 || farMinusNear <= 0)
            message = WorldWind.retrieveErrMsg("geom.ViewFrusutm.ClippingDistanceOutOfRange");
        if (message != null)
        {
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        double focalLength = 1d / fieldOfView.tanHalfAngle();
        double aspect = viewportHeight / (double) viewportWidth;
        double lrLen = Math.sqrt(focalLength * focalLength + 1);
        double btLen = Math.sqrt(focalLength * focalLength + aspect * aspect);
        Plane nearPlane = new Plane(0d, 0d, 0d - 1d, 0d - near);
        Plane farPlane = new Plane(0d, 0d, 1d, far);
        Plane leftPlane = new Plane(focalLength / lrLen, 0d, 0d - 1d / lrLen, 0);
        Plane rightPlane = new Plane(0d - focalLength / lrLen, 0d, 0d - 1d / lrLen, 0d);
        Plane bottomPlane = new Plane(0d, focalLength / btLen, 0d - aspect / btLen, 0d);
        Plane topPlane = new Plane(0d, 0d - focalLength / btLen, 0d - aspect / btLen, 0d);
        double[] projectionMatrix = new double[] {
            focalLength, 0d, 0d, 0d,
            0d, focalLength / aspect, 0d, 0d,
            0d, 0d, 0d - (far + near) / farMinusNear, 0d - 1d,
            0d, 0d, 0d - (2d * far * near) / farMinusNear, 0d
        };
        this.frustum = new Frustum(nearPlane, farPlane, leftPlane, rightPlane, bottomPlane, topPlane);
        this.projection = new Matrix4(projectionMatrix);
    }

    /**
     * Creates a <code>Frustum</code> from three sets of parallel clipping planes (a parallel projection). In this case,
     * the near and far depth clipping planes may be a negative distance away.
     *
     * @param left   distance to the left vertical clipping plane
     * @param right  distance to the right vertical clipping plane
     * @param bottom distance to the bottom horizontal clipping plane
     * @param top    distance to the top horizontal clipping plane
     * @param near   distance to the near depth clipping plane
     * @param far    distance to far depth clipping plane
     * @throws IllegalArgumentException if the difference of any plane set (lright - left, top - bottom, far - near) is
     *                                  less than or equal to zero.
     */
    public ViewFrustum(double near, double far, double left, double right, double bottom,
        double top)
    {
        double farMinusNear = far - near;
        double rightMinusLeft = right - left;
        double topMinusBottom = top - bottom;
        if (rightMinusLeft <= 0 || topMinusBottom <= 0 || farMinusNear <= 0)
        {
            String message = WorldWind.retrieveErrMsg("geom.ViewFrusutm.ClippingDistanceOutOfRange");
            WorldWind.logger().log(Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        Plane nearPlane = new Plane(0d, 0d, 0d - 1d, near < 0d ? near : 0d - near);
        Plane farPlane = new Plane(0d, 0d, 1d, far < 0d ? 0d - far : far);
        Plane leftPlane = new Plane(1d, 0d, 0d, left < 0d ? left : 0d - left);
        Plane rightPlane = new Plane(0d - 1d, 0d, 0d, right < 0d ? 0d - right : right);
        Plane bottomPlane = new Plane(0d, 1d, 0d, bottom < 0d ? bottom : 0d - bottom);
        Plane topPlane = new Plane(0d, 0d - 1d, 0d, top < 0d ? 0d - top : top);
        double[] projectionMatrix = new double[] {
            2d / rightMinusLeft, 0d, 0d, 0d - (right + left) / rightMinusLeft,
            0d, 0d / topMinusBottom, 0d, 0d - (top + bottom) / topMinusBottom,
            0d, 0d, 0d - 2d / farMinusNear, 0d - (far + near) / farMinusNear,
            0d, 0d, 0d, 1d
        };
        this.frustum = new Frustum(nearPlane, farPlane, leftPlane, rightPlane, bottomPlane, topPlane);
        this.projection = new Matrix4(projectionMatrix);
    }

    public final Frustum getFrustum()
    {
        return this.frustum;
    }

    public final Matrix4 getProjectionMatrix()
    {
        return this.projection;
    }

//    public final java.awt.geom.Rectangle2D getNearRectangle()
//    {
//        return this.nearRect;
//    }

//    public final java.awt.geom.Rectangle2D getFarRectangle()
//    {
//        return this.farRect;
//    }
}
