/*
 * Copyright (C) 2014 United States Government as represented by the Administrator of the
 * National Aeronautics and Space Administration. All Rights Reserved.
 */
/**
 * @exports Globe
 * @version $Id: Globe.js 2940 2015-03-30 17:58:36Z tgaskins $
 */
define([
        '../geom/Angle',
        '../error/ArgumentError',
        '../geom/BoundingBox',
        '../globe/ElevationModel',
        '../geom/Line',
        '../geom/Location',
        '../util/Logger',
        '../geom/Position',
        '../projections/ProjectionWgs84',
        '../geom/Sector',
        '../globe/Tessellator',
        '../geom/Vec3',
        '../util/WWMath'],
    function (Angle,
              ArgumentError,
              BoundingBox,
              ElevationModel,
              Line,
              Location,
              Logger,
              Position,
              ProjectionWgs84,
              Sector,
              Tessellator,
              Vec3,
              WWMath) {
        "use strict";

        /**
         * Constructs an ellipsoidal Globe with default radii for Earth (WGS84).
         * @alias Globe
         * @constructor
         * @classdesc Represents an ellipsoidal globe. The default configuration represents Earth but may be changed.
         * To configure for another planet, set the globe's equatorial and polar radii properties and its
         * eccentricity-squared property.
         * <p>
         * A globe uses a Cartesian coordinate system whose origin is at the globe's center. It's Y axis points to the
         * north pole, the Z axis points to the intersection of the prime meridian and the equator,
         * and the X axis completes a right-handed coordinate system, is in the equatorial plane and 90 degrees east
         * of the Z axis.
         * <p>
         *     All Cartesian coordinates and elevations are in meters.

         * @param {ElevationModel} elevationModel The elevation model to use for this globe.
         * @param {GeographicProjection} projection The projection to apply to the globe. May be null or undefined,
         * in which case no projection is applied and the globe is a WGS84 ellipsoid.
         * @throws {ArgumentError} If the specified elevation model is null or undefined.
         */
        var Globe = function (elevationModel, projection) {
            if (!elevationModel) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe",
                    "constructor", "Elevation model is null or undefined."));
            }
            /**
             * This globe's elevation model.
             * @type {ElevationModel}
             */
            this.elevationModel = elevationModel;

            /**
             * This globe's equatorial radius.
             * @type {Number}
             * @default 6378137.0 meters
             */
            this.equatorialRadius = 6378137.0;

            /**
             * This globe's polar radius.
             * @type {Number}
             * @default 6356752.3 meters
             */
            this.polarRadius = 6356752.3;

            /**
             * This globe's eccentricity squared.
             * @type {Number}
             * @default 0.00669437999013
             */
            this.eccentricitySquared = 0.00669437999013;

            /**
             * The tessellator used to create this globe's terrain.
             * @type {Tessellator}
             */
            this.tessellator = new Tessellator();

            // Internal. Intentionally not documented.
            this._projection = projection || new ProjectionWgs84();

            // Internal. Intentionally not documented.
            this._offset = 0;

            // Internal. Intentionally not documented.
            this.offsetVector = new Vec3(0, 0, 0);

            // A unique ID for this globe. Intentionally not documented.
            this.id = ++Globe.idPool;

            this._stateKey = "globe " + this.id.toString() + " ";
        };

        Globe.idPool = 0; // Used to assign unique IDs to globes for use in their state keys.

        Object.defineProperties(Globe.prototype, {
            /**
             * A string identifying this globe's current state. Used to compare states during rendering to
             * determine whether globe-state dependent cached values must be updated. Applications typically do not
             * interact with this property.
             * @memberof Globe.prototype
             * @readonly
             * @type {String}
             */
            stateKey: {
                get: function () {
                    return this._stateKey + this.elevationModel.stateKey + "offset " + this.offset.toString() + " "
                        + this.projection.stateKey;
                }
            },

            /**
             * Indicates whether this globe is 2D and continuous with itself -- that it should scroll continuously
             * horizontally.
             * @memberof Globe.prototype
             * @readonly
             * @type {Boolean}
             */
            continuous: {
                get: function () {
                    return this.projection.continuous;
                }
            },

            /**
             * The projection used by this globe.
             * @memberof Globe.prototype
             * @default {@link ProjectionWgs84}
             * @type {GeographicProjection}
             */
            projection: {
                get: function () {
                    return this._projection;
                },
                set: function (projection) {
                    if (!projection) {
                        throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe",
                            "projection", "missingProjection"));
                    }

                    if (this.projection != projection) {
                        this.tessellator = new Tessellator();
                    }
                    this._projection = projection;
                }
            },

            /**
             * The projection limits of the associated projection.
             * @memberof Globe.prototype
             * @type {Sector}
             */
            projectionLimits: {
                get: function () {
                    return this._projection.projectionLimits;
                }
            },

            /**
             * An offset to apply to this globe when translating between Geographic positions and Cartesian points.
             * Used during scrolling to position points appropriately.
             * Applications typically do not access this property. It is used by the associated globe.
             * @memberof Globe.prototype
             * @type {Number}
             */
            offset: {
                get: function () {
                    return this._offset;
                },
                set: function (offset) {
                    this._offset = offset;
                    this.offsetVector[0] = offset * 2 * Math.PI * this.equatorialRadius;
                }
            }
        });

        /**
         * Indicates whether this is a 2D globe.
         * @returns {Boolean} true if this is a 2D globe, otherwise false.
         */
        Globe.prototype.is2D = function () {
            return this.projection.is2D;
        };

        /**
         * Computes a Cartesian point from a specified position.
         * See this class' Overview section for a description of the Cartesian coordinate system used.
         * @param {Number} latitude The position's latitude.
         * @param {Number} longitude The position's longitude.
         * @param {Number} altitude The position's altitude.
         * @param {Vec3} result A reference to a pre-allocated {@link Vec3} in which to return the computed X,
         * Y and Z Cartesian coordinates.
         * @returns {Vec3} The result argument.
         * @throws {ArgumentError} If the specified result argument is null or undefined.
         */
        Globe.prototype.computePointFromPosition = function (latitude, longitude, altitude, result) {
            if (!result) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "computePointFromPosition",
                    "missingResult"));
            }

            return this.projection.geographicToCartesian(this, latitude, longitude, altitude, this.offsetVector, result);
        };

        /**
         * Computes a Cartesian point from a specified location.
         * See this class' Overview section for a description of the Cartesian coordinate system used.
         * @param {Number} latitude The position's latitude.
         * @param {Number} longitude The position's longitude.
         * @param {Vec3} result A reference to a pre-allocated {@link Vec3} in which to return the computed X,
         * Y and Z Cartesian coordinates.
         * @returns {Vec3} The result argument.
         * @throws {ArgumentError} If the specified result argument is null or undefined.
         */
        Globe.prototype.computePointFromLocation = function (latitude, longitude, result) {
            if (!result) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "computePointFromLocation",
                    "missingResult"));
            }

            return this.computePointFromPosition(latitude, longitude, 0, result);
        };

        /**
         * Computes a grid of Cartesian points within a specified sector and relative to a specified Cartesian
         * reference point.
         * <p>
         * This method is used to compute a collection of points within a sector. It is used by tessellators to
         * efficiently generate a tile's interior points. The number of points to generate is indicated by the numLon
         * and numLat parameters.
         * <p>
         * For each implied position within the sector, an elevation value is specified via an array of elevations. The
         * calculation at each position incorporates the associated elevation. There must be numLat x numLon elevations
         * in the array.
         *
         * @param {Sector} sector The sector for which to compute the points.
         * @param {Number} numLat The number of latitudinal points in the grid.
         * @param {Number} numLon The number of longitudinal points in the grid.
         * @param {Number[]} elevations An array of elevations to incorporate in the point calculations. There must be
         * one elevation value in the array for each generated point. Elevations are in meters. There must be
         * numLat x numLon elevations in the array.
         * @param {Vec3} referencePoint The X, Y and Z Cartesian coordinates to subtract from the computed coordinates.
         * This makes the computed coordinates relative to the specified point.
         * @param {Float32Array} result A typed array to hold the computed coordinates. It must be at least of
         * size numLat x numLon. The points are returned in row major order, beginning with the row of minimum latitude.
         * @returns {Float32Array} The specified result argument.
         * @throws {ArgumentError} if the specified sector, elevations array or results arrays are null or undefined, or
         * if the lengths of any of the arrays are insufficient.
         */
        Globe.prototype.computePointsForGrid = function (sector, numLat, numLon, elevations, referencePoint, result) {
            if (!sector) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe",
                    "computePointsFromPositions", "missingSector"));
            }

            if (numLat < 1 || numLon < 1) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "computePointsFromPositions",
                    "Number of latitude or longitude locations is less than one."));
            }

            var numPoints = numLat * numLon;
            if (!elevations || elevations.length < numPoints) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "computePointsFromPositions",
                    "Elevations array is null, undefined or insufficient length."));
            }

            if (!result || result.length < numPoints) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "computePointsFromPositions",
                    "Result array is null, undefined or insufficient length."));
            }

            return this.projection.geographicToCartesianGrid(this, sector, numLat, numLon, elevations, referencePoint,
                this.offsetVector, result);
        };

        /**
         * Computes a geographic position from a specified Cartesian point.
         *
         * See this class' Overview section for a description of the Cartesian coordinate system used.
         *
         * @param {Number} x The X coordinate.
         * @param {Number} y The Y coordinate.
         * @param {Number} z The Z coordinate.
         * @param {Position} result A pre-allocated {@link Position} instance in which to return the computed position.
         * @returns {Position} The specified result position.
         * @throws {ArgumentError} If the specified result argument is null or undefined.
         */
        Globe.prototype.computePositionFromPoint = function (x, y, z, result) {
            if (!result) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "computePositionFromPoint",
                    "missingResult"));
            }

            this.projection.cartesianToGeographic(this, x, y, z, this.offsetVector, result);

            // Wrap if the globe is continuous.
            if (this.continuous) {
                if (result.longitude < -180) {
                    result.longitude += 360;
                } else if (result.longitude > 180) {
                    result.longitude -= 360;
                }
            }

            return result;
        };

        /**
         * Computes the radius of this globe at a specified location.
         * @param {Number} latitude The locations' latitude.
         * @param {Number} longitude The locations' longitude.
         * @returns {Number} The radius at the specified location.
         */
        Globe.prototype.radiusAt = function (latitude, longitude) {
            var sinLat = Math.sin(latitude * Angle.DEGREES_TO_RADIANS),
                rpm = this.equatorialRadius / Math.sqrt(1.0 - this.eccentricitySquared * sinLat * sinLat);

            return rpm * Math.sqrt(1.0 + (this.eccentricitySquared * this.eccentricitySquared - 2.0 * this.eccentricitySquared) * sinLat * sinLat);
        };

        /**
         * Computes the normal vector to this globe's surface at a specified location.
         * @param {Number} latitude The location's latitude.
         * @param {Number} longitude The location's longitude.
         * @param {Vec3} result A pre-allocated {@Link Vec3} instance in which to return the computed vector. The returned
         * normal vector is unit length.
         * @returns {Vec3} The specified result vector.  The returned normal vector is unit length.
         * @throws {ArgumentError} If the specified result argument is null or undefined.
         */
        Globe.prototype.surfaceNormalAtLocation = function (latitude, longitude, result) {
            if (!result) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "surfaceNormalAtLocation",
                    "missingResult"));
            }

            if (this.is2D()) {
                result[0] = 0;
                result[1] = 0;
                result[2] = 1;

                return result;
            }

            var cosLat = Math.cos(latitude * Angle.DEGREES_TO_RADIANS),
                cosLon = Math.cos(longitude * Angle.DEGREES_TO_RADIANS),
                sinLat = Math.sin(latitude * Angle.DEGREES_TO_RADIANS),
                sinLon = Math.sin(longitude * Angle.DEGREES_TO_RADIANS),
                eqSquared = this.equatorialRadius * this.equatorialRadius,
                polSquared = this.polarRadius * this.polarRadius;

            result[0] = cosLat * sinLon / eqSquared;
            result[1] = (1 - this.eccentricitySquared) * sinLat / polSquared;
            result[2] = cosLat * cosLon / eqSquared;

            return result.normalize();
        };

        /**
         * Computes the normal vector to this globe's surface at a specified Cartesian point.
         * @param {Number} x The point's X coordinate.
         * @param {Number} y The point's Y coordinate.
         * @param {Number} z The point's Z coordinate.
         * @param {Vec3} result A pre-allocated {@Link Vec3} instance in which to return the computed vector. The returned
         * normal vector is unit length.
         * @returns {Vec3} The specified result vector.
         * @throws {ArgumentError} If the specified result argument is null or undefined.
         */
        Globe.prototype.surfaceNormalAtPoint = function (x, y, z, result) {
            if (!result) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "surfaceNormalAtPoint",
                    "missingResult"));
            }

            // For backwards compatibility, check whether the projection defines a surfaceNormalAtPoint function
            // before calling it. If it's not available, use the old code to compute the normal.
            if (this.projection.surfaceNormalAtPoint) {
                return this.projection.surfaceNormalAtPoint(this, x, y, z, result);
            }

            if (this.is2D()) {
                result[0] = 0;
                result[1] = 0;
                result[2] = 1;

                return result;
            }

            var eSquared = this.equatorialRadius * this.equatorialRadius,
                polSquared = this.polarRadius * this.polarRadius;

            result[0] = x / eSquared;
            result[1] = y / polSquared;
            result[2] = z / eSquared;

            return result.normalize();
        };

        /**
         * Computes the north-pointing tangent vector to this globe's surface at a specified location.
         * @param {Number} latitude The location's latitude.
         * @param {Number} longitude The location's longitude.
         * @param {Vec3} result A pre-allocated {@Link Vec3} instance in which to return the computed vector. The returned
         * tangent vector is unit length.
         * @returns {Vec3} The specified result vector.
         * @throws {ArgumentError} If the specified result argument is null or undefined.
         */
        Globe.prototype.northTangentAtLocation = function (latitude, longitude, result) {
            if (!result) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "northTangentAtLocation",
                    "missingResult"));
            }

            return this.projection.northTangentAtLocation(this, latitude, longitude, result);
        };

        /**
         * Computes the north-pointing tangent vector to this globe's surface at a specified Cartesian point.
         * @param {Number} x The point's X coordinate.
         * @param {Number} y The point's Y coordinate.
         * @param {Number} z The point's Z coordinate.
         * @param {Vec3} result A pre-allocated {@Link Vec3} instance in which to return the computed vector. The returned
         * tangent vector is unit length.
         * @returns {Vec3} The specified result vector.
         * @throws {ArgumentError} If the specified result argument is null or undefined.
         */
        Globe.prototype.northTangentAtPoint = function (x, y, z, result) {
            if (!result) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "northTangentAtPoint",
                    "missingResult"));
            }

            return this.projection.northTangentAtPoint(this, x, y, z, this.offsetVector, result);
        };

        /**
         * Indicates whether this globe intersects a specified frustum.
         * @param {Frustum} frustum The frustum to test.
         * @returns {Boolean} true if this globe intersects the frustum, otherwise false.
         * @throws {ArgumentError} If the specified frustum is null or undefined.
         */
        Globe.prototype.intersectsFrustum = function (frustum) {
            if (!frustum) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "intersectsFrustum", "missingFrustum"));
            }

            if (this.is2D()) {
                var bbox = new BoundingBox();
                bbox.setToSector(Sector.FULL_SPHERE, this, this.elevationModel.minElevation,
                    this.elevationModel.maxElevation);

                return bbox.intersectsFrustum(frustum);
            }

            if (frustum.far.distance <= this.equatorialRadius)
                return false;
            if (frustum.left.distance <= this.equatorialRadius)
                return false;
            if (frustum.right.distance <= this.equatorialRadius)
                return false;
            if (frustum.top.distance <= this.equatorialRadius)
                return false;
            if (frustum.bottom.distance <= this.equatorialRadius)
                return false;
            if (frustum.near.distance <= this.equatorialRadius)
                return false;

            return true;
        };

        /**
         * Computes the first intersection of this globe with a specified line. The line is interpreted as a ray;
         * intersection points behind the line's origin are ignored.
         * @param {Line} line The line to intersect with this globe.
         * @param {Vec3} result A pre-allocated Vec3 in which to return the computed point.
         * @returns {boolean} true If the ray intersects the globe, otherwise false.
         * @throws {ArgumentError} If the specified line or result argument is null or undefined.
         */
        Globe.prototype.intersectsLine = function (line, result) {
            if (!line) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "intersectWithRay", "missingLine"));
            }

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

            if (this.is2D()) {
                var vx = line.direction[0],
                    vy = line.direction[1],
                    vz = line.direction[2],
                    sx = line.origin[0],
                    sy = line.origin[1],
                    sz = line.origin[2],
                    t;

                if (vz == 0 && sz != 0) { // ray is parallel to and not coincident with the XY plane
                    return false;
                }

                t = -sz / vz; // intersection distance, simplified for the XY plane
                if (t < 0) { // intersection is behind the ray's origin
                    return false;
                }

                result[0] = sx + vx * t;
                result[1] = sy + vy * t;
                result[2] = sz + vz * t;

                return true;
            }

            return WWMath.computeEllipsoidalGlobeIntersection(line, this.equatorialRadius, this.polarRadius, result);
        };

        /**
         * Returns the time at which any elevations associated with this globe last changed.
         * @returns {Number} The time in milliseconds relative to the Epoch of the most recent elevation change.
         */
        Globe.prototype.elevationTimestamp = function () {
            return this.elevationModel.timestamp;
        };

        /**
         * Returns this globe's minimum elevation.
         * @returns {Number} This globe's minimum elevation.
         */
        Globe.prototype.minElevation = function () {
            return this.elevationModel.minElevation
        };

        /**
         * Returns this globe's maximum elevation.
         * @returns {Number} This globe's maximum elevation.
         */
        Globe.prototype.maxElevation = function () {
            return this.elevationModel.maxElevation
        };

        /**
         * Returns the minimum and maximum elevations within a specified sector of this globe.
         * @param {Sector} sector The sector for which to determine extreme elevations.
         * @returns {Number[]} The An array containing the minimum and maximum elevations.
         * @throws {ArgumentError} If the specified sector is null or undefined.
         */
        Globe.prototype.minAndMaxElevationsForSector = function (sector) {
            if (!sector) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "minAndMaxElevationsForSector",
                        "missingSector"));
            }

            return this.elevationModel.minAndMaxElevationsForSector(sector);
        };

        /**
         * Returns the elevation at a specified location.
         * @param {Number} latitude The location's latitude in degrees.
         * @param {Number} longitude The location's longitude in degrees.
         * @returns {Number} The elevation at the specified location, in meters. Returns zero if the location is
         * outside the coverage area of this elevation model.
         */
        Globe.prototype.elevationAtLocation = function (latitude, longitude) {
            return this.elevationModel.elevationAtLocation(latitude, longitude);
        };

        /**
         * Returns the elevations at locations within a specified sector.
         * @param {Sector} sector The sector for which to determine the elevations.
         * @param {Number} numLat The number of latitudinal sample locations within the sector.
         * @param {Number} numLon The number of longitudinal sample locations within the sector.
         * @param {Number} targetResolution The desired elevation resolution, in radians. (To compute radians from
         * meters, divide the number of meters by the globe's radius.)
         * @param {Number[]} result An array in which to return the requested elevations.
         * @returns {Number} The resolution actually achieved, which may be greater than that requested if the
         * elevation data for the requested resolution is not currently available.
         * @throws {ArgumentError} If the specified sector or result array is null or undefined, or if either of the
         * specified numLat or numLon values is less than one.
         */
        Globe.prototype.elevationsForGrid = function (sector, numLat, numLon, targetResolution, result) {
            if (!sector) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "elevationsForSector", "missingSector"));
            }

            if (numLat <= 0 || numLon <= 0) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe",
                    "elevationsForSector", "numLat or numLon is less than 1"));
            }

            if (!result || result.length < numLat * numLon) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe",
                    "elevationsForSector", "missingArray"));
            }

            return this.elevationModel.elevationsForGrid(sector, numLat, numLon, targetResolution, result);
        };

        return Globe;
    }
)
;