/*
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.*;
import gov.nasa.worldwind.geom.Point;

import javax.media.opengl.*;
import javax.media.opengl.glu.*;
import java.awt.*;
import java.util.logging.Level;

/**
 * @author Paul Collins
 * @version $Id: AbstractView.java 1794 2007-05-08 22:07:21Z dcollins $
 */
public abstract class AbstractView extends WWObjectImpl implements View
{
    private static final Double DefaultFov;

    static
    {
        DefaultFov = Configuration.getDoubleValue(AVKey.FOV, 45d);
    }

    // Current OpenGL viewing state.
    private Matrix4 modelView;
    private Matrix4 projection;
    private Rectangle viewport;
    private Angle fieldOfView = Angle.fromDegrees(DefaultFov);
    // Current DrawContext state.
    private Globe globe = null;
    private double verticalExaggeration = -1;
    // Cached viewing attribute computations.
    private Point eye = null;
    private Point up = null;
    private Point forward = null;
    private Frustum frustumInModelCoords = null;
    private double pixelSizeScale = -1;
    private double horizonDistance = -1;
    // Temporary state.
    private static final int[] matrixMode = new int[1];
    private static final int[] viewportArray = new int[4];

    public void apply(DrawContext dc)
    {
        validateDrawContext(dc);
        this.globe = dc.getGlobe();
        this.verticalExaggeration = dc.getVerticalExaggeration();

        // Get the current OpenGL viewport state.
        dc.getGL().glGetIntegerv(GL.GL_VIEWPORT, viewportArray, 0);
        this.viewport = new Rectangle(viewportArray[0], viewportArray[1], viewportArray[2], viewportArray[3]);

        this.clearCachedAttributes();
        this.doApply(dc);
    }

    protected abstract void doApply(DrawContext dc);

    private void clearCachedAttributes()
    {
        this.eye = null;
        this.up = null;
        this.forward = null;
        this.frustumInModelCoords = null;
        this.pixelSizeScale = -1;
        this.horizonDistance = -1;
    }

    protected void applyMatrixState(DrawContext dc, Matrix4 modelView, Matrix4 projection)
    {
        validateDrawContext(dc);
        if (modelView == null)
        {
            String message = WorldWind.retrieveErrMsg("AbstractView.ModelViewIsNull");
            WorldWind.logger().log(Level.FINE, message);
        }
        if (projection == null)
        {
            String message = WorldWind.retrieveErrMsg("AbstractView.ProjectionIsNull");
            WorldWind.logger().log(Level.FINE, message);
        }

        GL gl = dc.getGL();

        // Store the current matrix-mode state.
        gl.glGetIntegerv(GL.GL_MATRIX_MODE, matrixMode, 0);
        int newMatrixMode = matrixMode[0];

        // Apply the model-view matrix to the current OpenGL context held by 'dc'.
        if (newMatrixMode != GL.GL_MODELVIEW)
        {
            newMatrixMode = GL.GL_MODELVIEW;
            gl.glMatrixMode(newMatrixMode);
        }
        if (modelView != null)
            gl.glLoadMatrixd(modelView.getEntries(), 0);
        else
            gl.glLoadIdentity();

        // Apply the projection matrix to the current OpenGL context held by 'dc'.
        newMatrixMode = GL.GL_PROJECTION;
        gl.glMatrixMode(newMatrixMode);
        if (projection != null)
            gl.glLoadMatrixd(projection.getEntries(), 0);
        else
            gl.glLoadIdentity();

        // Restore matrix-mode state.
        if (newMatrixMode != matrixMode[0])
            gl.glMatrixMode(matrixMode[0]);

        this.modelView = modelView;
        this.projection = projection;
    }

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

        if (dc.getGL() == null)
        {
            String message = WorldWind.retrieveErrMsg("AbstractView.DrawingContextGLIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalStateException(message);
        }
    }

    public Matrix4 getModelViewMatrix()
    {
        return this.modelView;
    }

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

    public java.awt.Rectangle getViewport()
    {
        return this.viewport;
    }

    public Frustum getFrustumInModelCoordinates()
    {
        if (this.frustumInModelCoords == null)
        {
            // Compute the current model-view coordinate frustum.
            Frustum frust = this.getFrustum();
            if (frust != null && this.modelView != null)
                this.frustumInModelCoords = frust.getInverseTransformed(this.modelView);
        }
        return this.frustumInModelCoords;
    }

    public Angle getFieldOfView()
    {
        return this.fieldOfView;
    }

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

    public void pushReferenceCenter(DrawContext dc, Point referenceCenter)
    {
        validateDrawContext(dc);
        if (referenceCenter == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.PointIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        Matrix4 newModelView;
        if (this.modelView != null)
        {
            newModelView = new Matrix4(this.modelView.getEntries());
            Matrix4 reference = new Matrix4();
            reference.translate(referenceCenter);
            newModelView.multiply(reference);
        }
        else
        {
            newModelView = new Matrix4();
        }

        GL gl = dc.getGL();
        // Store the current matrix-mode state.
        gl.glGetIntegerv(GL.GL_MATRIX_MODE, matrixMode, 0);
        // Push and load a new model-view matrix to the current OpenGL context held by 'dc'.
        if (matrixMode[0] != GL.GL_MODELVIEW)
            gl.glMatrixMode(GL.GL_MODELVIEW);
        gl.glPushMatrix();
        gl.glLoadMatrixd(newModelView.getEntries(), 0);
        // Restore matrix-mode state.
        if (matrixMode[0] != GL.GL_MODELVIEW)
            gl.glMatrixMode(matrixMode[0]);
    }

    public void popReferenceCenter(DrawContext dc)
    {
        validateDrawContext(dc);
        GL gl = dc.getGL();
        // Store the current matrix-mode state.
        gl.glGetIntegerv(GL.GL_MATRIX_MODE, matrixMode, 0);
        // Pop a model-view matrix off the current OpenGL context held by 'dc'.
        if (matrixMode[0] != GL.GL_MODELVIEW)
            gl.glMatrixMode(GL.GL_MODELVIEW);
        gl.glPopMatrix();
        // Restore matrix-mode state.
        if (matrixMode[0] != GL.GL_MODELVIEW)
            gl.glMatrixMode(matrixMode[0]);
    }

    public Point getEyePoint()
    {
        if (this.eye == null)
        {
            Matrix modelViewInv;
            if (this.modelView != null && (modelViewInv = this.modelView.getInverse()) != null)
                this.eye = modelViewInv.transform(new Point(0, 0, 0, 1));
        }
        return this.eye;
    }

    public Point getUpVector()
    {
        if (this.up == null)
        {
            Matrix modelViewInv;
            if (this.modelView != null && (modelViewInv = this.modelView.getInverse()) != null)
                this.up = modelViewInv.transform(new Point(0, 1, 0, 0));
        }
        return this.up;
    }

    public Point getForwardVector()
    {
        if (this.forward == null)
        {
            Matrix modelViewInv;
            if (this.modelView != null && (modelViewInv = this.modelView.getInverse()) != null)
                this.forward = modelViewInv.transform(new Point(0, 0, -1, 0));
        }
        return this.forward;
    }

    // TODO: this should be expressed in OpenGL screen coordinates, not toolkit (e.g. AWT) coordinates
    public Line computeRayFromScreenPoint(double x, double y)
    {
        if (this.viewport == null)
            return null;
        double yInv = this.viewport.height - y - 1; // TODO: should be computed by caller
        Point a = this.unProject(new Point(x, yInv, 0, 0));
        Point b = this.unProject(new Point(x, yInv, 1, 0));
        if (a == null || b == null)
            return null;
        return new Line(a, b.subtract(a).normalize());
    }

    // TODO: rename?, remove?
    public Position computePositionFromScreenPoint(double x, double y)
    {
        Line line = this.computeRayFromScreenPoint(x, y);
        if (line == null)
            return null;
        if (this.globe == null)
            return null;
        return this.globe.getIntersectionPosition(line);
    }

    public double computePixelSizeAtDistance(double distance)
    {
        if (this.pixelSizeScale < 0)
        {
            // Compute the current coefficient for computing the size of a pixel.
            if (this.fieldOfView != null && this.viewport.width > 0)
                this.pixelSizeScale = 2 * fieldOfView.tanHalfAngle() / (double) this.viewport.width;
            else if (this.viewport.width > 0)
                this.pixelSizeScale = 1 / (double) this.viewport.width;
        }
        if (this.pixelSizeScale < 0)
            return -1;
        return this.pixelSizeScale * Math.abs(distance);
    }

    public double computeHorizonDistance()
    {
        if (this.horizonDistance < 0)
        {
            this.horizonDistance = this.computeHorizonDistance(this.globe, this.verticalExaggeration,
                this.getEyePoint());
        }
        return this.horizonDistance;
    }

    protected double computeHorizonDistance(Globe globe, double verticalExaggeration, Point eyePoint)
    {
        if (globe == null || eyePoint == null)
            return -1;

        // Compute the current (approximate) distance from eye to globe horizon.
        Position eyePosition = globe.computePositionFromPoint(eyePoint);
        double elevation = verticalExaggeration
            * globe.getElevation(eyePosition.getLatitude(), eyePosition.getLongitude());
        Point surface = globe.computePointFromPosition(eyePosition.getLatitude(), eyePosition.getLongitude(),
            elevation);
        double altitude = eyePoint.length() - surface.length();
        double radius = globe.getMaximumRadius();
        return Math.sqrt(altitude * (2 * radius + altitude));
    }

    public Point project(Point modelPoint)
    {
        if (modelPoint == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.PointIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }
        if (this.modelView == null || this.projection == null || this.viewport == null)
            return null;
        Point eyeCoord = this.modelView.transform(new Point(modelPoint.x(), modelPoint.y(), modelPoint.z(), 1));
        Point clipCoord = this.projection.transform(eyeCoord);
        if (clipCoord.w() == 0)
            return null;
        Point normDeviceCoord = new Point(clipCoord.x() / clipCoord.w(), clipCoord.y() / clipCoord.w(),
            clipCoord.z() / clipCoord.w(), 0);
        return new Point(
            (normDeviceCoord.x() + 1) * (this.viewport.width / 2d) + this.viewport.x,
            (normDeviceCoord.y() + 1) * (this.viewport.height / 2d) + this.viewport.y,
            (normDeviceCoord.z() + 1) / 2d,
            0);
    }

    public Point unProject(Point windowPoint)
    {
        if (windowPoint == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.PointIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        if (this.modelView == null || this.projection == null || this.viewport == null)
            return null;
        double[] projectionMatrix = this.projection.getEntries();
        double[] modelViewMatrix = this.modelView.getEntries();
        int[] viewport = new int[] {this.viewport.x, this.viewport.y, this.viewport.width, this.viewport.height};
        double[] modelPoint = new double[3];
        GLU glu = new GLU();
        if (glu.gluUnProject(windowPoint.x(), windowPoint.y(), windowPoint.z(), modelViewMatrix, 0, projectionMatrix, 0,
            viewport, 0, modelPoint, 0))
            return new Point(modelPoint[0], modelPoint[1], modelPoint[2], 0d);
        else
            return null;

        // TODO: uncomment this when Matrix4.getInverse() is fixed
//        if (projection == null || modelView == null || viewport == null)
//            return null;
//        Point ndCoord = new Point(
//            2 * (windowPoint.x() - viewport.getX()) / (double) viewport.width - 1,
//            2 * (windowPoint.y() - viewport.getY()) / (double) viewport.height - 1,
//            2 * windowPoint.z() - 1,
//            1);
//        Matrix m = new Matrix4(modelView.getEntries());
//        m.multiply(projection);
//        Point clipCoord = m.getInverse().transform(ndCoord);
//        if (clipCoord.w() == 0)
//            return null;
//        return new Point(clipCoord.x() / clipCoord.w(), clipCoord.y() / clipCoord.w(),
//            clipCoord.z() / clipCoord.w(), 0);
    }
}
