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

import gov.nasa.worldwind.geom.*;

/**
 * @author dcollins
 * @version $Id: BasicOrbitView.java 1794 2007-05-08 22:07:21Z dcollins $
 */
public class BasicOrbitView extends AbstractView
{
    private static final Double DefaultLatitude;
    private static final Double DefaultLongitude;
    private static final Double DefaultZoom;
    private static final Double DefaultMinZoom;
    private static final Double DefaultMaxZoom;
    private static final Boolean DefaultEnableZoomConstraint;
    private static final Double DefaultHeading;
    private static final Double DefaultPitch;
    private static final Double DefaultMinPitch;
    private static final Double DefaultMaxPitch;
    private static final Boolean DefaultEnablePitchConstraint;

    static
    {
        // Default latitude and longitude.
        java.util.TimeZone tz = java.util.Calendar.getInstance().getTimeZone();
        DefaultLatitude = Configuration.getDoubleValue(AVKey.INITIAL_LATITUDE, 0d);
        DefaultLongitude = Configuration.getDoubleValue(AVKey.INITIAL_LONGITUDE,
            (180d * tz.getOffset(System.currentTimeMillis()) / (12d * 3.6e6)));
        // Default zoom/eye-distance.
        DefaultZoom = 0d;
        DefaultMinZoom = 0d;
        DefaultMaxZoom = Double.POSITIVE_INFINITY;
        DefaultEnableZoomConstraint = Boolean.TRUE;
        // Default heading and pitch.
        DefaultHeading = 0d;
        DefaultPitch = 0d;
        DefaultMinPitch = 0d;
        DefaultMaxPitch = 90d;
        DefaultEnablePitchConstraint = Boolean.TRUE;
    }

    // Geographic coordinate data.
    private Angle focusLat = Angle.fromDegrees(DefaultLatitude);
    private Angle focusLon = Angle.fromDegrees(DefaultLongitude);
    private double eyeDist = DefaultZoom;
    private Angle heading = Angle.fromDegrees(DefaultHeading);
    private Angle pitch = Angle.fromDegrees(DefaultPitch);
    private double altitude;
    // Coordinate constraints.
    private double minEyeDist = DefaultMinZoom;
    private double maxEyeDist = DefaultMaxZoom;
    private boolean enableZoomConstraint = DefaultEnableZoomConstraint;
    private Angle minPitch = Angle.fromDegrees(DefaultMinPitch);
    private Angle maxPitch = Angle.fromDegrees(DefaultMaxPitch);
    private boolean enablePitchConstraint = DefaultEnablePitchConstraint;
    // Current OpenGL projection state.
    private ViewFrustum viewFrustum;
    private double collisionRadius;

    private boolean isInitialized = false;

    protected void doApply(DrawContext dc)
    {
        if (!isInitialized)
        {
            this.doInitialize(dc);
            isInitialized = true;
        }

        Matrix4 modelView, projection = null;
        // Compute the current model-view matrix and view eye point.
        modelView = this.computeModelViewMatrix(dc);
        Point eyePoint = modelView.getInverse().transform(new Point(0, 0, 0, 1));
        // Compute the current viewing frustum and projection matrix.
        this.viewFrustum = this.computeViewFrustum(dc, eyePoint);
        if (this.viewFrustum != null)
        {
            this.collisionRadius = this.computeCollisionRadius(this.viewFrustum);
            projection = this.viewFrustum.getProjectionMatrix();
        }
        // Set current GL matrix state.
        this.applyMatrixState(dc, modelView, projection);
    }

    private void doInitialize(DrawContext dc)
    {
        Globe globe = dc.getGlobe();

        // Set the coordinate constraints to default values.
        this.minEyeDist = this.collisionRadius = 1;
        if (globe != null)
            this.maxEyeDist = 6 * globe.getRadius();
        else
            this.maxEyeDist = Double.POSITIVE_INFINITY;

        // Set the eye distance to a default value.
        if (globe != null)
            this.eyeDist = this.clampZoom(3 * globe.getRadius());
    }

    private Matrix4 computeModelViewMatrix(DrawContext dc)
    {
        Globe globe = dc.getGlobe();
        if (globe == null)
            return null;

        Point focusPoint = globe.computePointFromPosition(this.focusLat, this.focusLon, 0);
        if (focusPoint == null)
        {
            String message = WorldWind.retrieveErrMsg("BasicOrbitView.NullSurfacePoint");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalStateException(message);
        }

        Matrix4 modelView = lookAt(this.focusLat, this.focusLon, focusPoint.length(),
            this.eyeDist, this.heading, this.pitch);

        Point eye = modelView.getInverse().transform(new Point(0, 0, 0, 1));

        Position polarEye = globe.computePositionFromPoint(eye);
        Point surfacePoint = computeSurfacePoint(dc, polarEye.getLatitude(), polarEye.getLongitude());
        if (surfacePoint != null)
        {
            double distanceToSurface = eye.length() - this.collisionRadius - surfacePoint.length();
            if (distanceToSurface < 0)
            {
                Point surfaceNormal = eye.normalize();
                Point newEye = Point.fromOriginAndDirection(eye.length() - distanceToSurface, surfaceNormal,
                    Point.ZERO);
                Point forward = eye.subtract(focusPoint);
                Point newForward = newEye.subtract(focusPoint);
                double dot = forward.dot(newForward) / (forward.length() * newForward.length());
                if (dot >= -1 && dot <= 1)
                {
                    double pitchChange = Math.acos(dot);
                    this.pitch = this.clampPitch(this.pitch.subtract(Angle.fromRadians(pitchChange)));
                    this.eyeDist = this.clampZoom(newForward.length());
                    modelView = lookAt(this.focusLat, this.focusLon, focusPoint.length(), this.eyeDist,
                        this.heading, this.pitch);
                }
            }
        }

        // Compute the current eye altitude above sea level (Globe radius).
        eye = modelView.getInverse().transform(new Point(0, 0, 0, 1));
        polarEye = globe.computePositionFromPoint(eye);
        this.altitude = eye.length() - globe.getRadiusAt(polarEye.getLatitude(), polarEye.getLongitude());

        return modelView;
    }

    private static Matrix4 lookAt(Angle focusX, Angle focusY, double focusDistance,
        double tiltDistance, Angle tiltZ, Angle tiltX)
    {
        Matrix4 m = new Matrix4();
        // Translate model away from eye.
        m.translate(0, 0, -tiltDistance);
        // Apply tilt by rotating about X axis at pivot point.
        m.rotateX(tiltX.multiply(-1));
        m.rotateZ(tiltZ);
        m.translate(0, 0, -focusDistance);
        // Rotate model to lat/lon of eye point.
        m.rotateX(focusX);
        m.rotateY(focusY.multiply(-1));
        return m;
    }

    private static Point computeSurfacePoint(DrawContext dc, Angle lat, Angle lon)
    {
        Point p = null;

        SectorGeometryList geom = dc.getSurfaceGeometry();
        if (geom != null)
            p = geom.getSurfacePoint(lat, lon);
        if (p != null)
            return p;

        Globe globe = dc.getGlobe();
        if (globe != null)
        {
            double elevation = dc.getVerticalExaggeration() * globe.getElevation(lat, lon);
            p = globe.computePointFromPosition(lat, lon, elevation);
        }

        return p;
    }

    public Frustum getFrustum()
    {
        if (this.viewFrustum == null)
            return null;
        return this.viewFrustum.getFrustum();
    }

    private ViewFrustum computeViewFrustum(DrawContext dc, Point eyePoint)
    {
        java.awt.Rectangle viewport = this.getViewport();
        Angle fov = this.getFieldOfView();
        if (viewport == null || fov == null)
            return null;
        // Compute the most distant near clipping plane.
        double tanHalfFov = fov.tanHalfAngle();
        double near = Math.max(10, this.altitude / (2 * Math.sqrt(2 * tanHalfFov * tanHalfFov + 1)));
        // Compute the closest allowable far clipping plane distance.
        double far = this.computeHorizonDistance(dc.getGlobe(), dc.getVerticalExaggeration(), eyePoint);
        // Compute the frustum from a standard perspective projection.
        return new ViewFrustum(fov, viewport.width, viewport.height, near, far);
    }

    private double computeCollisionRadius(ViewFrustum viewFrustum)
    {
        java.awt.Rectangle viewport = this.getViewport();
        Angle fov = this.getFieldOfView();
        if (viewport == null
            || fov == null
            || viewFrustum == null
            || viewFrustum.getFrustum() == null
            || viewFrustum.getFrustum().getNear() == null)
            return 1;

        double near = Math.abs(viewFrustum.getFrustum().getNear().getDistance());
        if (near == 0)
            near = 1;

        double tanHalfFov = fov.tanHalfAngle();
        // Compute the distance between the eye, and any corner on the near clipping rectangle.
        double clipRectX = near * tanHalfFov;
        double clipRectY = viewport.height * clipRectX / (double) viewport.width;
        return 1 + Math.sqrt(clipRectX * clipRectX + clipRectY * clipRectY + near * near);
    }

    public Position getPosition()
    {
        return new Position(this.focusLat, this.focusLon, 0);
    }

    public void goToLatLon(LatLon newLatLon)
    {
        if (newLatLon == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.LatLonIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        LatLon clampedLatLon = this.clampCoordinate(newLatLon);
        this.focusLat = clampedLatLon.getLatitude();
        this.focusLon = clampedLatLon.getLongitude();
    }

    public double getAltitude()
    {
        return this.altitude;
    }

    public void goToAltitude(double newAltitude)
    {
        throw new UnsupportedOperationException();
    }

    public void goToCoordinate(LatLon newLatLon, double newAltitude)
    {
        throw new UnsupportedOperationException();
    }

    private LatLon clampCoordinate(LatLon latLon)
    {
        if (latLon == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.LatLonIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        double lat = latLon.getLatitude().getDegrees();
        if (lat < -90)
            lat = -90;
        else if (lat > 90)
            lat = 90;

        double lon = latLon.getLongitude().getDegrees();
        if (lon < -180)
            lon = lon + 360;
        else if (lon > 180)
            lon = lon - 360;

        return LatLon.fromDegrees(lat, lon);
    }

    public Angle getHeading()
    {
        return this.heading;
    }

    public void setHeading(Angle newHeading)
    {
        if (newHeading == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.AngleIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }
        this.heading = this.clampHeading(newHeading);
    }

    private Angle clampHeading(Angle heading)
    {
        if (heading == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.AngleIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }
        double degrees = heading.getDegrees();
        if (degrees < 0)
            degrees = degrees + 360;
        else if (degrees > 360)
            degrees = degrees - 360;
        return Angle.fromDegrees(degrees);
    }

    public Angle getPitch()
    {
        return this.pitch;
    }

    public void setPitch(Angle newPitch)
    {
        if (newPitch == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.AngleIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }
        this.pitch = this.clampPitch(newPitch);
    }

    public Angle[] getPitchConstraints()
    {
        return new Angle[] {this.minPitch, this.maxPitch};
    }

    public void setPitchConstraints(Angle newMinPitch, Angle newMaxPitch)
    {
        if (newMinPitch.compareTo(newMaxPitch) < 0)
        {
            String message = WorldWind.retrieveErrMsg("BasicOrbitView.InvalidConstraints");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }
        this.minPitch = newMinPitch;
        this.maxPitch = newMaxPitch;
    }

    public boolean isEnablePitchConstraints()
    {
        return this.enablePitchConstraint;
    }

    public void setEnablePitchConstraints(boolean enabled)
    {
        this.enablePitchConstraint = enabled;
    }

    private Angle clampPitch(Angle pitch)
    {
        Angle[] constraints = this.getPitchConstraints();
        if (pitch.compareTo(constraints[0]) < 0)
            this.pitch = constraints[0];
        else if (pitch.compareTo(constraints[1]) > 0)
            this.pitch = constraints[1];
        return this.pitch = pitch;
    }

    public Angle getRoll()
    {
        return Angle.ZERO;
    }

    public void setRoll(Angle newRoll)
    {
    }

    public double getZoom()
    {
        return this.eyeDist;
    }

    public void setZoom(double newZoom)
    {
        this.eyeDist = this.clampZoom(newZoom);
    }

    public double[] getZoomConstraints()
    {
        return new double[] {Math.max(this.minEyeDist, this.collisionRadius), this.maxEyeDist};
    }

    public void setZoomConstraints(double newMinZoom, double newMaxZoom)
    {
        if (newMinZoom < 0 || newMinZoom > newMaxZoom)
        {
            String message = WorldWind.retrieveErrMsg("BasicOrbitView.InvalidConstraints");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }
        this.minEyeDist = newMinZoom;
        this.maxEyeDist = newMaxZoom;
    }

    public boolean isEnableZoomConstraints()
    {
        return this.enableZoomConstraint;
    }

    public void setEnableZoomConstraints(boolean enabled)
    {
        this.enableZoomConstraint = enabled;
    }

    private double clampZoom(double zoom)
    {
        double x = zoom;
        double[] constraints = this.getZoomConstraints();
        if (x < constraints[0])
            x = constraints[0];
        else if (x > constraints[1])
            x = constraints[1];
        return x;
    }

    public LatLon computeVisibleLatLonRange()
    {
        return null;  //To change body of implemented methods use File | Settings | File Templates.
    }
}
