/*global define*/
define([
        '../Core/BoundingSphere',
        '../Core/Cartesian3',
        '../Core/Cartesian4',
        '../Core/defaultValue',
        '../Core/defined',
        '../Core/defineProperties',
        '../Core/DeveloperError',
        '../Core/Ellipsoid',
        '../Core/HeadingPitchRange',
        '../Core/JulianDate',
        '../Core/Math',
        '../Core/Matrix3',
        '../Core/Matrix4',
        '../Core/Transforms',
        '../Scene/SceneMode'
    ], function(
        BoundingSphere,
        Cartesian3,
        Cartesian4,
        defaultValue,
        defined,
        defineProperties,
        DeveloperError,
        Ellipsoid,
        HeadingPitchRange,
        JulianDate,
        CesiumMath,
        Matrix3,
        Matrix4,
        Transforms,
        SceneMode) {
    "use strict";

    var updateTransformMatrix3Scratch1 = new Matrix3();
    var updateTransformMatrix3Scratch2 = new Matrix3();
    var updateTransformMatrix3Scratch3 = new Matrix3();
    var updateTransformMatrix4Scratch = new Matrix4();
    var updateTransformCartesian3Scratch1 = new Cartesian3();
    var updateTransformCartesian3Scratch2 = new Cartesian3();
    var updateTransformCartesian3Scratch3 = new Cartesian3();
    var updateTransformCartesian3Scratch4 = new Cartesian3();
    var updateTransformCartesian3Scratch5 = new Cartesian3();
    var updateTransformCartesian3Scratch6 = new Cartesian3();
    var deltaTime = new JulianDate();
    var northUpAxisFactor = 1.25;  // times ellipsoid's maximum radius

    function updateTransform(that, camera, updateLookAt, saveCamera, positionProperty, time, ellipsoid) {
        var mode = that.scene.mode;
        var cartesian = positionProperty.getValue(time, that._lastCartesian);
        if (defined(cartesian)) {
            var hasBasis = false;
            var xBasis;
            var yBasis;
            var zBasis;

            if (mode === SceneMode.SCENE3D) {
                // The time delta was determined based on how fast satellites move compared to vehicles near the surface.
                // Slower moving vehicles will most likely default to east-north-up, while faster ones will be VVLH.
                deltaTime = JulianDate.addSeconds(time, 0.001, deltaTime);
                var deltaCartesian = positionProperty.getValue(deltaTime, updateTransformCartesian3Scratch1);
                if (defined(deltaCartesian)) {
                    var toInertial = Transforms.computeFixedToIcrfMatrix(time, updateTransformMatrix3Scratch1);
                    var toInertialDelta = Transforms.computeFixedToIcrfMatrix(deltaTime, updateTransformMatrix3Scratch2);
                    var toFixed;

                    if (!defined(toInertial) || !defined(toInertialDelta)) {
                        toFixed = Transforms.computeTemeToPseudoFixedMatrix(time, updateTransformMatrix3Scratch3);
                        toInertial = Matrix3.transpose(toFixed, updateTransformMatrix3Scratch1);
                        toInertialDelta = Transforms.computeTemeToPseudoFixedMatrix(deltaTime, updateTransformMatrix3Scratch2);
                        Matrix3.transpose(toInertialDelta, toInertialDelta);
                    } else {
                        toFixed = Matrix3.transpose(toInertial, updateTransformMatrix3Scratch3);
                    }

                    var inertialCartesian = Matrix3.multiplyByVector(toInertial, cartesian, updateTransformCartesian3Scratch5);
                    var inertialDeltaCartesian = Matrix3.multiplyByVector(toInertialDelta, deltaCartesian, updateTransformCartesian3Scratch6);

                    Cartesian3.subtract(inertialCartesian, inertialDeltaCartesian, updateTransformCartesian3Scratch4);
                    var inertialVelocity = Cartesian3.magnitude(updateTransformCartesian3Scratch4) * 1000.0; // meters/sec

                    // http://en.wikipedia.org/wiki/Standard_gravitational_parameter
                    // Consider adding this to Cesium.Ellipsoid?
                    var mu = 3.986004418e14; // m^3 / sec^2

                    var semiMajorAxis = -mu / (inertialVelocity * inertialVelocity - (2 * mu / Cartesian3.magnitude(inertialCartesian)));

                    if (semiMajorAxis < 0 || semiMajorAxis > northUpAxisFactor * ellipsoid.maximumRadius) {
                        // North-up viewing from deep space.

                        // X along the nadir
                        xBasis = updateTransformCartesian3Scratch2;
                        Cartesian3.normalize(cartesian, xBasis);
                        Cartesian3.negate(xBasis, xBasis);

                        // Z is North
                        zBasis = Cartesian3.clone(Cartesian3.UNIT_Z, updateTransformCartesian3Scratch3);

                        // Y is along the cross of z and x (right handed basis / in the direction of motion)
                        yBasis = Cartesian3.cross(zBasis, xBasis, updateTransformCartesian3Scratch1);
                        if (Cartesian3.magnitude(yBasis) > CesiumMath.EPSILON7) {
                            Cartesian3.normalize(xBasis, xBasis);
                            Cartesian3.normalize(yBasis, yBasis);

                            zBasis = Cartesian3.cross(xBasis, yBasis, updateTransformCartesian3Scratch3);
                            Cartesian3.normalize(zBasis, zBasis);

                            hasBasis = true;
                        }
                    } else if (!Cartesian3.equalsEpsilon(cartesian, deltaCartesian, CesiumMath.EPSILON7)) {
                        // Approximation of VVLH (Vehicle Velocity Local Horizontal) with the Z-axis flipped.

                        // Z along the position
                        zBasis = updateTransformCartesian3Scratch2;
                        Cartesian3.normalize(inertialCartesian, zBasis);
                        Cartesian3.normalize(inertialDeltaCartesian, inertialDeltaCartesian);

                        // Y is along the angular momentum vector (e.g. "orbit normal")
                        yBasis = Cartesian3.cross(zBasis, inertialDeltaCartesian, updateTransformCartesian3Scratch3);
                        if (!Cartesian3.equalsEpsilon(yBasis, Cartesian3.ZERO, CesiumMath.EPSILON7)) {
                            // X is along the cross of y and z (right handed basis / in the direction of motion)
                            xBasis = Cartesian3.cross(yBasis, zBasis, updateTransformCartesian3Scratch1);

                            Matrix3.multiplyByVector(toFixed, xBasis, xBasis);
                            Matrix3.multiplyByVector(toFixed, yBasis, yBasis);
                            Matrix3.multiplyByVector(toFixed, zBasis, zBasis);

                            Cartesian3.normalize(xBasis, xBasis);
                            Cartesian3.normalize(yBasis, yBasis);
                            Cartesian3.normalize(zBasis, zBasis);

                            hasBasis = true;
                        }
                    }
                }
            }

            if (defined(that._boundingSphereOffset)) {
                Cartesian3.add(that._boundingSphereOffset, cartesian, cartesian);
            }

            var position;
            var direction;
            var up;

            if (saveCamera) {
                position = Cartesian3.clone(camera.position, updateTransformCartesian3Scratch4);
                direction = Cartesian3.clone(camera.direction, updateTransformCartesian3Scratch5);
                up = Cartesian3.clone(camera.up, updateTransformCartesian3Scratch6);
            }

            var transform = updateTransformMatrix4Scratch;
            if (hasBasis) {
                transform[0]  = xBasis.x;
                transform[1]  = xBasis.y;
                transform[2]  = xBasis.z;
                transform[3]  = 0.0;
                transform[4]  = yBasis.x;
                transform[5]  = yBasis.y;
                transform[6]  = yBasis.z;
                transform[7]  = 0.0;
                transform[8]  = zBasis.x;
                transform[9]  = zBasis.y;
                transform[10] = zBasis.z;
                transform[11] = 0.0;
                transform[12]  = cartesian.x;
                transform[13]  = cartesian.y;
                transform[14] = cartesian.z;
                transform[15] = 0.0;
            } else {
                // Stationary or slow-moving, low-altitude objects use East-North-Up.
                Transforms.eastNorthUpToFixedFrame(cartesian, ellipsoid, transform);
            }

            camera._setTransform(transform);

            if (saveCamera) {
                Cartesian3.clone(position, camera.position);
                Cartesian3.clone(direction, camera.direction);
                Cartesian3.clone(up, camera.up);
                Cartesian3.cross(direction, up, camera.right);
            }
        }

        if (updateLookAt) {
            var offset = (mode === SceneMode.SCENE2D || Cartesian3.equals(that._offset3D, Cartesian3.ZERO)) ? undefined : that._offset3D;
            camera.lookAtTransform(camera.transform, offset);
        }
    }

    /**
     * A utility object for tracking an entity with the camera.
     * @alias EntityView
     * @constructor
     *
     * @param {Entity} entity The entity to track with the camera.
     * @param {Scene} scene The scene to use.
     * @param {Ellipsoid} [ellipsoid=Ellipsoid.WGS84] The ellipsoid to use for orienting the camera.
     * @param {BoundingSphere} [boundingSphere] An initial bounding sphere for setting the default view.
     */
    function EntityView(entity, scene, ellipsoid, boundingSphere) {

        /**
         * The entity to track with the camera.
         * @type {Entity}
         */
        this.entity = entity;

        /**
         * The scene in which to track the object.
         * @type {Scene}
         */
        this.scene = scene;

        /**
         * The ellipsoid to use for orienting the camera.
         * @type {Ellipsoid}
         */
        this.ellipsoid = defaultValue(ellipsoid, Ellipsoid.WGS84);

        /**
         * Gets or sets an initial bounding sphere for viewing the entity.
         * @type {Entity}
         */
        this.boundingSphere = BoundingSphere.clone(boundingSphere);

        this._boundingSphereOffset = undefined;

        //Shadow copies of the objects so we can detect changes.
        this._lastEntity = undefined;
        this._mode = undefined;

        this._lastCartesian = new Cartesian3();
        this._defaultOffset3D = undefined;

        this._offset3D = new Cartesian3();
    }

    // STATIC properties defined here, not per-instance.
    defineProperties(EntityView, {
        /**
         * Gets or sets a camera offset that will be used to
         * initialize subsequent EntityViews.
         * @memberof EntityView
         * @type {Cartesian3}
         */
        defaultOffset3D : {
            get : function() {
                return this._defaultOffset3D;
            },
            set : function(vector) {
                this._defaultOffset3D = Cartesian3.clone(vector, new Cartesian3());
            }
        }
    });

    // Initialize the static property.
    EntityView.defaultOffset3D = new Cartesian3(-14000, 3500, 3500);

    var scratchHeadingPitchRange = new HeadingPitchRange();
    var scratchCartesian = new Cartesian3();

    /**
    * Should be called each animation frame to update the camera
    * to the latest settings.
    * @param {JulianDate} time The current animation time.
    *
    */
    EntityView.prototype.update = function(time) {
        var scene = this.scene;
        var entity = this.entity;
        var ellipsoid = this.ellipsoid;

        //>>includeStart('debug', pragmas.debug);
        if (!defined(time)) {
            throw new DeveloperError('time is required.');
        }
        if (!defined(scene)) {
            throw new DeveloperError('EntityView.scene is required.');
        }
        if (!defined(entity)) {
            throw new DeveloperError('EntityView.entity is required.');
        }
        if (!defined(ellipsoid)) {
            throw new DeveloperError('EntityView.ellipsoid is required.');
        }
        if (!defined(entity.position)) {
            throw new DeveloperError('entity.position is required.');
        }
        //>>includeEnd('debug');

        var sceneMode = scene.mode;
        if (sceneMode === SceneMode.MORPHING) {
            return;
        }

        var positionProperty = entity.position;
        var objectChanged = entity !== this._lastEntity;
        var sceneModeChanged = sceneMode !== this._mode;

        var offset3D = this._offset3D;
        var camera = scene.camera;

        var updateLookAt = objectChanged || sceneModeChanged;
        var saveCamera = true;

        if (objectChanged) {
            var viewFromProperty = entity.viewFrom;
            var hasViewFrom = defined(viewFromProperty);
            var sphere = this.boundingSphere;
            this._boundingSphereOffset = undefined;

            if (!hasViewFrom && defined(sphere)) {
                var controller = scene.screenSpaceCameraController;
                controller.minimumZoomDistance = Math.min(controller.minimumZoomDistance, sphere.radius * 0.5);

                //The default HPR is not ideal for high altitude objects so
                //we scale the pitch as we get further from the earth for a more
                //downward view.
                scratchHeadingPitchRange.pitch = -CesiumMath.PI_OVER_FOUR;
                scratchHeadingPitchRange.range = 0;
                var position = positionProperty.getValue(time, scratchCartesian);
                if (defined(position)) {
                    var factor = 2 - 1 / Math.max(1, Cartesian3.magnitude(position) / ellipsoid.maximumRadius);
                    scratchHeadingPitchRange.pitch *= factor;
                }

                camera.viewBoundingSphere(sphere, scratchHeadingPitchRange);
                this._boundingSphereOffset = Cartesian3.subtract(sphere.center, entity.position.getValue(time), new Cartesian3());
                updateLookAt = false;
                saveCamera = false;
            } else if (!hasViewFrom || !defined(viewFromProperty.getValue(time, offset3D))) {
                Cartesian3.clone(EntityView._defaultOffset3D, offset3D);
            }
        } else if (!sceneModeChanged && scene.mode !== SceneMode.MORPHING && this._mode !== SceneMode.SCENE2D) {
            Cartesian3.clone(camera.position, offset3D);
        }

        this._lastEntity = entity;
        this._mode = scene.mode !== SceneMode.MORPHING ? scene.mode : this._mode;

        if (scene.mode !== SceneMode.MORPHING) {
            updateTransform(this, camera, updateLookAt, saveCamera, positionProperty, time, ellipsoid);
        }
    };

    return EntityView;
});
