/*
 * Copyright 2015-2017 WorldWind Contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/**
 * @exports NavigatorState
 */
define([
        '../error/ArgumentError',
        '../geom/Frustum',
        '../geom/Line',
        '../util/Logger',
        '../geom/Matrix',
        '../geom/Rectangle',
        '../geom/Vec2',
        '../geom/Vec3',
        '../util/WWMath'
    ],
    function (ArgumentError,
              Frustum,
              Line,
              Logger,
              Matrix,
              Rectangle,
              Vec2,
              Vec3,
              WWMath) {
        "use strict";

        /**
         * Constructs a navigator state. This constructor is meant to be called by navigators when their current state
         * is requested.
         * @alias NavigatorState
         * @constructor
         * @classdesc Represents the state of a navigator.
         * <p>
         * Properties of NavigatorState objects are
         * read-only because they are values captured from a {@link Navigator}. Setting the properties on
         * a NavigatorState instance has no effect on the Navigator from which they came.
         * @param {Matrix} modelViewMatrix The navigator's model-view matrix.
         * @param {Matrix} projectionMatrix The navigator's projection matrix.
         * @param {Rectangle} viewport The navigator's viewport.
         * @param {Number} heading The navigator's heading.
         * @param {Number} tilt The navigator's tilt.
         */
        var NavigatorState = function (modelViewMatrix, projectionMatrix, viewport, heading, tilt) {

            /**
             * The navigator's model-view matrix. The model-view matrix transforms points from model coordinates to eye
             * coordinates.
             * @type {Matrix}
             * @readonly
             */
            this.modelview = modelViewMatrix;

            /**
             * The navigator's projection matrix. The projection matrix transforms points from eye coordinates to clip
             * coordinates.
             * @type {Matrix}
             * @readonly
             */
            this.projection = projectionMatrix;

            /**
             * The concatenation of the navigator's model-view and projection matrices. This matrix transforms points
             * from model coordinates to clip coordinates.
             * @type {Matrix}
             * @readonly
             */
            this.modelviewProjection = Matrix.fromIdentity();
            this.modelviewProjection.setToMultiply(projectionMatrix, modelViewMatrix);

            /**
             * The navigator's viewport, in WebGL screen coordinates. The viewport places the origin in the bottom-left
             * corner and has axes that extend up and to the right from the origin.
             * @type {Rectangle}
             * @readonly
             */
            this.viewport = viewport;

            /**
             * Indicates the number of degrees clockwise from north to which the view is directed.
             * @type {Number}
             * @readonly
             */
            this.heading = heading;

            /**
             * The number of degrees the globe is tilted relative to its surface being parallel to the screen. Values are
             * typically in the range 0 to 90 but may vary from that depending on the navigator in use.
             * @type {Number}
             * @readonly
             */
            this.tilt = tilt;

            /**
             * The navigator's eye point in model coordinates, relative to the globe's center.
             * @type {Vec3}
             * @readonly
             */
            this.eyePoint = this.modelview.extractEyePoint(new Vec3(0, 0, 0));

            /**
             * The navigator's viewing frustum in model coordinates. The frustum originates at the eyePoint and extends
             * outward along the forward vector. The navigator's near distance and far distance identify the minimum and
             * maximum distance, respectively, at which an object in the scene is visible.
             * @type {Frustum}
             * @readonly
             */
            this.frustumInModelCoordinates = null;
            // Compute the frustum in model coordinates. Start by computing the frustum in eye coordinates from the
            // projection matrix, then transform this frustum to model coordinates by multiplying its planes by the
            // transpose of the modelview matrix. We use the transpose of the modelview matrix because planes are
            // transformed by the inverse transpose of a matrix, and we want to transform from eye coordinates to model
            // coordinates.
            var modelviewTranspose = Matrix.fromIdentity();
            modelviewTranspose.setToTransposeOfMatrix(this.modelview);
            this.frustumInModelCoordinates = Frustum.fromProjectionMatrix(this.projection);
            this.frustumInModelCoordinates.transformByMatrix(modelviewTranspose);
            this.frustumInModelCoordinates.normalize();

            // Compute the inverse of the modelview, projection, and modelview-projection matrices. The inverse matrices
            // are used to support operations on navigator state, such as project, unProject, and pixelSizeAtDistance.
            this.modelviewInv = Matrix.fromIdentity();
            this.modelviewInv.invertOrthonormalMatrix(this.modelview);
            this.projectionInv = Matrix.fromIdentity();
            this.projectionInv.invertMatrix(this.projection);
            this.modelviewProjectionInv = Matrix.fromIdentity();
            this.modelviewProjectionInv.invertMatrix(this.modelviewProjection);

            /**
             * The matrix that transforms normal vectors in model coordinates to normal vectors in eye coordinates.
             * Typically used to transform a shape's normal vectors during lighting calculations.
             * @type {Matrix}
             * @readonly
             */
            this.modelviewNormalTransform = Matrix.fromIdentity().setToTransposeOfMatrix(this.modelviewInv.upper3By3());

            // Compute the eye coordinate rectangles carved out of the frustum by the near and far clipping planes, and
            // the distance between those planes and the eye point along the -Z axis. The rectangles are determined by
            // transforming the bottom-left and top-right points of the frustum from clip coordinates to eye
            // coordinates.
            var nbl = new Vec3(-1, -1, -1),
                ntr = new Vec3(+1, +1, -1),
                fbl = new Vec3(-1, -1, +1),
                ftr = new Vec3(+1, +1, +1);
            // Convert each frustum corner from clip coordinates to eye coordinates by multiplying by the inverse
            // projection matrix.
            nbl.multiplyByMatrix(this.projectionInv);
            ntr.multiplyByMatrix(this.projectionInv);
            fbl.multiplyByMatrix(this.projectionInv);
            ftr.multiplyByMatrix(this.projectionInv);

            var nrRectWidth = WWMath.fabs(ntr[0] - nbl[0]),
                frRectWidth = WWMath.fabs(ftr[0] - fbl[0]),
                nrDistance = -nbl[2],
                frDistance = -fbl[2];

            // Compute the scale and offset used to determine the width of a pixel on a rectangle carved out of the
            // frustum at a distance along the -Z axis in eye coordinates. These values are found by computing the scale
            // and offset of a frustum rectangle at a given distance, then dividing each by the viewport width.
            var frustumWidthScale = (frRectWidth - nrRectWidth) / (frDistance - nrDistance),
                frustumWidthOffset = nrRectWidth - frustumWidthScale * nrDistance;
            this.pixelSizeScale = frustumWidthScale / viewport.width;
            this.pixelSizeOffset = frustumWidthOffset / viewport.height;
        };

        /**
         * Transforms the specified model point from model coordinates to WebGL screen coordinates.
         * <p>
         * The resultant screen point is in WebGL screen coordinates, with the origin in the bottom-left corner and
         * axes that extend up and to the right from the origin.
         * <p>
         * This function stores the transformed point in the result argument, and returns true or false to indicate
         * whether or not the transformation is successful. It returns false if this navigator state's modelview or
         * projection matrices are malformed, or if the specified model point is clipped by the near clipping plane or
         * the far clipping plane.
         *
         * @param {Vec3} modelPoint The model coordinate point to project.
         * @param {Vec3} result A pre-allocated vector in which to return the projected point.
         * @returns {boolean} true if the transformation is successful, otherwise false.
         * @throws {ArgumentError} If either the specified point or result argument is null or undefined.
         */
        NavigatorState.prototype.project = function (modelPoint, result) {
            if (!modelPoint) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "NavigatorState", "project",
                    "missingPoint"));
            }

            if (!result) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "NavigatorState", "project",
                    "missingResult"));
            }

            // Transform the model point from model coordinates to eye coordinates then to clip coordinates. This
            // inverts the Z axis and stores the negative of the eye coordinate Z value in the W coordinate.
            var mx = modelPoint[0],
                my = modelPoint[1],
                mz = modelPoint[2],
                m = this.modelviewProjection,
                x = m[0] * mx + m[1] * my + m[2] * mz + m[3],
                y = m[4] * mx + m[5] * my + m[6] * mz + m[7],
                z = m[8] * mx + m[9] * my + m[10] * mz + m[11],
                w = m[12] * mx + m[13] * my + m[14] * mz + m[15],
                viewport = this.viewport;

            if (w == 0) {
                return false;
            }

            // Complete the conversion from model coordinates to clip coordinates by dividing by W. The resultant X, Y
            // and Z coordinates are in the range [-1,1].
            x /= w;
            y /= w;
            z /= w;

            // Clip the point against the near and far clip planes.
            if (z < -1 || z > 1) {
                return false;
            }

            // Convert the point from clip coordinate to the range [0,1]. This enables the X and Y coordinates to be
            // converted to screen coordinates, and the Z coordinate to represent a depth value in the range[0,1].
            x = x * 0.5 + 0.5;
            y = y * 0.5 + 0.5;
            z = z * 0.5 + 0.5;

            // Convert the X and Y coordinates from the range [0,1] to screen coordinates.
            x = x * viewport.width + viewport.x;
            y = y * viewport.height + viewport.y;

            result[0] = x;
            result[1] = y;
            result[2] = z;

            return true;
        };
        /**
         * Transforms the specified model point from model coordinates to WebGL screen coordinates, applying an offset
         * to the modelPoint's projected depth value.
         * <p>
         * The resultant screen point is in WebGL screen coordinates, with the origin in the bottom-left corner and axes
         * that extend up and to the right from the origin.
         * <p>
         * This function stores the transformed point in the result argument, and returns true or false to indicate whether or
         * not the transformation is successful. It returns false if this navigator state's modelview or projection
         * matrices are malformed, or if the modelPoint is clipped by the near clipping plane or the far clipping plane,
         * ignoring the depth offset.
         * <p>
         * The depth offset may be any real number and is typically used to move the screenPoint slightly closer to the
         * user's eye in order to give it visual priority over nearby objects or terrain. An offset of zero has no effect.
         * An offset less than zero brings the screenPoint closer to the eye, while an offset greater than zero pushes the
         * projected screen point away from the eye.
         * <p>
         * Applying a non-zero depth offset has no effect on whether the model point is clipped by this method or by
         * WebGL. Clipping is performed on the original model point, ignoring the depth offset. The final depth value
         * after applying the offset is clamped to the range [0,1].
         *
         * @param {Vec3} modelPoint The model coordinate point to project.
         * @param {Number} depthOffset The amount of offset to apply.
         * @param {Vec3} result A pre-allocated vector in which to return the projected point.
         * @returns {boolean} true if the transformation is successful, otherwise false.
         * @throws {ArgumentError} If either the specified point or result argument is null or undefined.
         */
        NavigatorState.prototype.projectWithDepth = function (modelPoint, depthOffset, result) {
            if (!modelPoint) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "NavigatorState", "projectWithDepth",
                    "missingPoint"));
            }

            if (!result) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "NavigatorState", "projectWithDepth",
                    "missingResult"));
            }

            // Transform the model point from model coordinates to eye coordinates. The eye coordinate and the clip
            // coordinate are transformed separately in order to reuse the eye coordinate below.
            var mx = modelPoint[0],
                my = modelPoint[1],
                mz = modelPoint[2],
                m = this.modelview,
                ex = m[0] * mx + m[1] * my + m[2] * mz + m[3],
                ey = m[4] * mx + m[5] * my + m[6] * mz + m[7],
                ez = m[8] * mx + m[9] * my + m[10] * mz + m[11],
                ew = m[12] * mx + m[13] * my + m[14] * mz + m[15];

            // Transform the point from eye coordinates to clip coordinates.
            var p = this.projection,
                x = p[0] * ex + p[1] * ey + p[2] * ez + p[3] * ew,
                y = p[4] * ex + p[5] * ey + p[6] * ez + p[7] * ew,
                z = p[8] * ex + p[9] * ey + p[10] * ez + p[11] * ew,
                w = p[12] * ex + p[13] * ey + p[14] * ez + p[15] * ew,
                viewport = this.viewport;

            if (w === 0) {
                return false;
            }

            // Complete the conversion from model coordinates to clip coordinates by dividing by W. The resultant X, Y
            // and Z coordinates are in the range [-1,1].
            x /= w;
            y /= w;
            z /= w;

            // Clip the point against the near and far clip planes.
            if (z < -1 || z > 1) {
                return false;
            }

            // Transform the Z eye coordinate to clip coordinates again, this time applying a depth offset. The depth
            // offset is applied only to the matrix element affecting the projected Z coordinate, so we inline the
            // computation here instead of re-computing X, Y, Z and W in order to improve performance. See
            // Matrix.offsetProjectionDepth for more information on the effect of this offset.
            z = p[8] * ex + p[9] * ey + p[10] * ez * (1 + depthOffset) + p[11] * ew;
            z /= w;

            // Clamp the point to the near and far clip planes. We know the point's original Z value is contained within
            // the clip planes, so we limit its offset z value to the range [-1, 1] in order to ensure it is not clipped
            // by WebGL. In clip coordinates the near and far clip planes are perpendicular to the Z axis and are
            // located at -1 and 1, respectively.
            z = WWMath.clamp(z, -1, 1);

            // Convert the point from clip coordinates to the range [0, 1]. This enables the XY coordinates to be
            // converted to screen coordinates, and the Z coordinate to represent a depth value in the range [0, 1].
            x = x * 0.5 + 0.5;
            y = y * 0.5 + 0.5;
            z = z * 0.5 + 0.5;

            // Convert the X and Y coordinates from the range [0,1] to screen coordinates.
            x = x * viewport.width + viewport.x;
            y = y * viewport.height + viewport.y;

            result[0] = x;
            result[1] = y;
            result[2] = z;

            return true;
        };

        /**
         * Transforms the specified screen point from WebGL screen coordinates to model coordinates.
         * <p>
         * The screen point is understood to be in WebGL screen coordinates, with the origin in the bottom-left corner
         * and axes that extend up and to the right from the origin.
         * <p>
         * This function stores the transformed point in the result argument, and returns true or false to indicate whether the
         * transformation is successful. It returns false if this navigator state's modelview or projection matrices
         * are malformed, or if the screenPoint is clipped by the near clipping plane or the far clipping plane.
         *
         * @param {Vec3} screenPoint The screen coordinate point to un-project.
         * @param {Vec3} result A pre-allocated vector in which to return the unprojected point.
         * @returns {boolean} true if the transformation is successful, otherwise false.
         * @throws {ArgumentError} If either the specified point or result argument is null or undefined.
         */
        NavigatorState.prototype.unProject = function (screenPoint, result) {
            if (!screenPoint) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "NavigatorState", "unProject",
                    "missingPoint"));
            }

            if (!result) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "NavigatorState", "unProject",
                    "missingResult"));
            }

            var sx = screenPoint[0],
                sy = screenPoint[1],
                sz = screenPoint[2],
                viewport = this.viewport;

            // Convert the XY screen coordinates to coordinates in the range [0, 1]. This enables the XY coordinates to
            // be converted to clip coordinates.
            sx = (sx - viewport.x) / viewport.width;
            sy = (sy - viewport.y) / viewport.height;

            // Convert from coordinates in the range [0, 1] to clip coordinates in the range [-1, 1].
            sx = sx * 2 - 1;
            sy = sy * 2 - 1;
            sz = sz * 2 - 1;

            // Clip the point against the near and far clip planes. In clip coordinates the near and far clip planes are
            // perpendicular to the Z axis and are located at -1 and 1, respectively.
            if (sz < -1 || sz > 1) {
                return false;
            }

            // Transform the screen point from clip coordinates to model coordinates. This inverts the Z axis and stores
            // the negative of the eye coordinate Z value in the W coordinate.
            var m = this.modelviewProjectionInv,
                x = m[0] * sx + m[1] * sy + m[2] * sz + m[3],
                y = m[4] * sx + m[5] * sy + m[6] * sz + m[7],
                z = m[8] * sx + m[9] * sy + m[10] * sz + m[11],
                w = m[12] * sx + m[13] * sy + m[14] * sz + m[15];

            if (w === 0) {
                return false;
            }

            // Complete the conversion from model coordinates to clip coordinates by dividing by W.
            result[0] = x / w;
            result[1] = y / w;
            result[2] = z / w;

            return true;
        };

        /**
         * Converts a WebGL screen point to window coordinates.
         * <p>
         * The specified point is understood to be in WebGL screen coordinates, with the origin in the bottom-left
         * corner and axes that extend up and to the right from the origin point.
         * <p>
         * The returned point is in the window coordinate system of the WorldWindow, with the origin in the top-left
         * corner and axes that extend down and to the right from the origin point.
         *
         * @param {Vec2} screenPoint The screen point to convert.
         * @param {Vec2} result A pre-allocated {@link Vec2} in which to return the computed point.
         * @returns {Vec2} The specified result argument set to the computed point.
         * @throws {ArgumentError} If either argument is null or undefined.
         */
        NavigatorState.prototype.convertPointToWindow = function (screenPoint, result) {
            if (!screenPoint) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "NavigatorState", "convertPointToWindow",
                    "missingPoint"));
            }

            if (!result) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "NavigatorState", "convertPointToWindow",
                    "missingResult"));
            }

            result[0] = screenPoint[0];
            result[1] = this.viewport.height - screenPoint[1];

            return result;
        };

        /**
         * Converts a window-coordinate point to WebGL screen coordinates.
         * <p>
         * The specified point is understood to be in the window coordinate system of the WorldWindow, with the origin
         * in the top-left corner and axes that extend down and to the right from the origin point.
         * <p>
         * The returned point is in WebGL screen coordinates, with the origin in the bottom-left corner and axes that
         * extend up and to the right from the origin point.
         *
         * @param {Vec2} point The window-coordinate point to convert.
         * @param {Vec2} result A pre-allocated {@link Vec2} in which to return the computed point.
         * @returns {Vec2} The specified result argument set to the computed point.
         * @throws {ArgumentError} If either argument is null or undefined.
         */
        NavigatorState.prototype.convertPointToViewport = function (point, result) {
            if (!point) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "NavigatorState", "convertPointToViewport",
                    "missingPoint"));
            }

            if (!result) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "NavigatorState", "convertPointToViewport",
                    "missingResult"));
            }

            result[0] = point[0];
            result[1] = this.viewport.height - point[1];

            return result;
        };

        /**
         * Computes a ray originating at the navigator's eyePoint and extending through the specified point in window
         * coordinates.
         * <p>
         * The specified point is understood to be in the window coordinate system of the WorldWindow, with the origin
         * in the top-left corner and axes that extend down and to the right from the origin point.
         * <p>
         * The results of this method are undefined if the specified point is outside of the WorldWindow's
         * bounds.
         *
         * @param {Vec2} point The window coordinates point to compute a ray for.
         * @returns {Line} A new Line initialized to the origin and direction of the computed ray, or null if the
         * ray could not be computed.
         */
        NavigatorState.prototype.rayFromScreenPoint = function (point) {
            if (!point) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "NavigatorState", "rayFromScreenPoint",
                    "missingPoint"));
            }

            // Convert the point's xy coordinates from window coordinates to WebGL screen coordinates.
            var screenPoint = this.convertPointToViewport(point, new Vec3(0, 0, 0)),
                nearPoint = new Vec3(0, 0, 0),
                farPoint = new Vec3(0, 0, 0);

            // Compute the model coordinate point on the near clip plane with the xy coordinates and depth 0.
            if (!this.unProject(screenPoint, nearPoint)) {
                return null;
            }

            // Compute the model coordinate point on the far clip plane with the xy coordinates and depth 1.
            screenPoint[2] = 1;
            if (!this.unProject(screenPoint, farPoint)) {
                return null;
            }

            // Compute a ray originating at the eye point and with direction pointing from the xy coordinate on the near
            // plane to the same xy coordinate on the far plane.
            var origin = new Vec3(this.eyePoint[0], this.eyePoint[1], this.eyePoint[2]),
                direction = new Vec3(farPoint[0], farPoint[1], farPoint[2]);

            direction.subtract(nearPoint);
            direction.normalize();

            return new Line(origin, direction);
        };

        /**
         * Computes the approximate size of a pixel at a specified distance from the navigator's eye point.
         * <p>
         * This method assumes rectangular pixels, where pixel coordinates denote
         * infinitely thin spaces between pixels. The units of the returned size are in model coordinates per pixel
         * (usually meters per pixel). This returns 0 if the specified distance is zero. The returned size is undefined
         * if the distance is less than zero.
         *
         * @param {Number} distance The distance from the eye point at which to determine pixel size, in model
         * coordinates.
         * @returns {Number} The approximate pixel size at the specified distance from the eye point, in model
         * coordinates per pixel.
         */
        NavigatorState.prototype.pixelSizeAtDistance = function (distance) {
            // Compute the pixel size from the width of a rectangle carved out of the frustum in model coordinates at
            // the specified distance along the -Z axis and the viewport width in screen coordinates. The pixel size is
            // expressed in model coordinates per screen coordinate (e.g. meters per pixel).
            //
            // The frustum width is determined by noticing that the frustum size is a linear function of distance from
            // the eye point. The linear equation constants are determined during initialization, then solved for
            // distance here.
            //
            // This considers only the frustum width by assuming that the frustum and viewport share the same aspect
            // ratio, so that using either the frustum width or height results in the same pixel size.

            return this.pixelSizeScale * distance + this.pixelSizeOffset;
        };

        return NavigatorState;
    });