/*global define*/
define([
        '../Core/Cartesian3',
        '../Core/Cartographic',
        '../Core/defined',
        '../Core/destroyObject',
        '../Core/DeveloperError',
        '../Core/EasingFunction',
        '../Core/Math',
        '../Core/Matrix4',
        '../Core/Ray',
        '../Core/ScreenSpaceEventHandler',
        '../Core/ScreenSpaceEventType',
        '../Core/Transforms',
        './Camera',
        './OrthographicFrustum',
        './PerspectiveFrustum',
        './SceneMode'
    ], function(
        Cartesian3,
        Cartographic,
        defined,
        destroyObject,
        DeveloperError,
        EasingFunction,
        CesiumMath,
        Matrix4,
        Ray,
        ScreenSpaceEventHandler,
        ScreenSpaceEventType,
        Transforms,
        Camera,
        OrthographicFrustum,
        PerspectiveFrustum,
        SceneMode) {
    'use strict';

    /**
     * @private
     */
    function SceneTransitioner(scene) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(scene)) {
            throw new DeveloperError('scene is required.');
        }
        //>>includeEnd('debug');

        this._scene = scene;
        this._currentTweens = [];
        this._morphHandler = undefined;
        this._morphCancelled = false;
        this._completeMorph = undefined;
    }

    SceneTransitioner.prototype.completeMorph = function() {
        if (defined(this._completeMorph)) {
            this._completeMorph();
        }
    };

    SceneTransitioner.prototype.morphTo2D = function(duration, ellipsoid) {
        if (defined(this._completeMorph)) {
            this._completeMorph();
        }

        var scene = this._scene;
        this._previousMode = scene.mode;

        if (this._previousMode === SceneMode.SCENE2D || this._previousMode === SceneMode.MORPHING) {
            return;
        }
        this._scene.morphStart.raiseEvent(this, this._previousMode, SceneMode.SCENE2D, true);

        scene._mode = SceneMode.MORPHING;
        scene.camera._setTransform(Matrix4.IDENTITY);

        if (this._previousMode === SceneMode.COLUMBUS_VIEW) {
            morphFromColumbusViewTo2D(this, duration);
        } else {
            morphFrom3DTo2D(this, duration, ellipsoid);
        }

        if (duration === 0.0 && defined(this._completeMorph)) {
            this._completeMorph();
        }
    };

    var scratchToCVPosition = new Cartesian3();
    var scratchToCVDirection = new Cartesian3();
    var scratchToCVUp = new Cartesian3();
    var scratchToCVPosition2D = new Cartesian3();
    var scratchToCVDirection2D = new Cartesian3();
    var scratchToCVUp2D = new Cartesian3();
    var scratchToCVSurfacePosition = new Cartesian3();
    var scratchToCVCartographic = new Cartographic();
    var scratchToCVToENU = new Matrix4();
    var scratchToCVFrustum = new PerspectiveFrustum();
    var scratchToCVCamera = {
        position : undefined,
        direction : undefined,
        up : undefined,
        position2D : undefined,
        direction2D : undefined,
        up2D : undefined,
        frustum : undefined
    };

    SceneTransitioner.prototype.morphToColumbusView = function(duration, ellipsoid) {
        if (defined(this._completeMorph)) {
            this._completeMorph();
        }

        var scene = this._scene;
        this._previousMode = scene.mode;

        if (this._previousMode === SceneMode.COLUMBUS_VIEW || this._previousMode === SceneMode.MORPHING) {
            return;
        }
        this._scene.morphStart.raiseEvent(this, this._previousMode, SceneMode.COLUMBUS_VIEW, true);

        scene.camera._setTransform(Matrix4.IDENTITY);

        var position = scratchToCVPosition;
        var direction = scratchToCVDirection;
        var up = scratchToCVUp;

        if (duration > 0.0) {
            position.x = 0.0;
            position.y = 0.0;
            position.z = 5.0 * ellipsoid.maximumRadius;

            Cartesian3.negate(Cartesian3.UNIT_Z, direction);
            Cartesian3.clone(Cartesian3.UNIT_Y, up);
        } else {
            var camera = scene.camera;
            if (this._previousMode === SceneMode.SCENE2D) {
                Cartesian3.clone(camera.position, position);
                position.z = camera.frustum.right - camera.frustum.left;
                Cartesian3.negate(Cartesian3.UNIT_Z, direction);
                Cartesian3.clone(Cartesian3.UNIT_Y, up);
            } else {
                Cartesian3.clone(camera.positionWC, position);
                Cartesian3.clone(camera.directionWC, direction);
                Cartesian3.clone(camera.upWC, up);

                var surfacePoint = ellipsoid.scaleToGeodeticSurface(position, scratchToCVSurfacePosition);
                var toENU = Transforms.eastNorthUpToFixedFrame(surfacePoint, ellipsoid, scratchToCVToENU);
                Matrix4.inverseTransformation(toENU, toENU);

                scene.mapProjection.project(ellipsoid.cartesianToCartographic(position, scratchToCVCartographic), position);
                Matrix4.multiplyByPointAsVector(toENU, direction, direction);
                Matrix4.multiplyByPointAsVector(toENU, up, up);
            }
        }

        var frustum = scratchToCVFrustum;
        frustum.aspectRatio = scene.drawingBufferWidth / scene.drawingBufferHeight;
        frustum.fov = CesiumMath.toRadians(60.0);

        var cameraCV = scratchToCVCamera;
        cameraCV.position = position;
        cameraCV.direction = direction;
        cameraCV.up = up;
        cameraCV.frustum = frustum;

        var complete = completeColumbusViewCallback(cameraCV);
        createMorphHandler(this, complete);

        if (this._previousMode === SceneMode.SCENE2D) {
            morphFrom2DToColumbusView(this, duration, cameraCV, complete);
        } else {
            cameraCV.position2D = Matrix4.multiplyByPoint(Camera.TRANSFORM_2D, position, scratchToCVPosition2D);
            cameraCV.direction2D = Matrix4.multiplyByPointAsVector(Camera.TRANSFORM_2D, direction, scratchToCVDirection2D);
            cameraCV.up2D = Matrix4.multiplyByPointAsVector(Camera.TRANSFORM_2D, up, scratchToCVUp2D);

            scene._mode = SceneMode.MORPHING;
            morphFrom3DToColumbusView(this, duration, cameraCV, complete);
        }

        if (duration === 0.0 && defined(this._completeMorph)) {
            this._completeMorph();
        }
    };

    SceneTransitioner.prototype.morphTo3D = function(duration, ellipsoid) {
        if (defined(this._completeMorph)) {
            this._completeMorph();
        }

        var scene = this._scene;
        this._previousMode = scene.mode;

        if (this._previousMode === SceneMode.SCENE3D || this._previousMode === SceneMode.MORPHING) {
            return;
        }
        this._scene.morphStart.raiseEvent(this, this._previousMode, SceneMode.SCENE3D, true);

        scene._mode = SceneMode.MORPHING;
        scene.camera._setTransform(Matrix4.IDENTITY);

        if (this._previousMode === SceneMode.SCENE2D) {
            morphFrom2DTo3D(this, duration, ellipsoid);
        } else {
            var camera3D;
            if (duration > 0.0) {
                camera3D = scratchCVTo3DCamera;
                Cartesian3.fromDegrees(0.0, 0.0, 5.0 * ellipsoid.maximumRadius, ellipsoid, camera3D.position);
                Cartesian3.negate(camera3D.position, camera3D.direction);
                Cartesian3.normalize(camera3D.direction, camera3D.direction);
                Cartesian3.clone(Cartesian3.UNIT_Z, camera3D.up);
            } else {
                camera3D = getColumbusViewTo3DCamera(this, ellipsoid);
            }
            var complete = complete3DCallback(camera3D);
            createMorphHandler(this, complete);

            morphFromColumbusViewTo3D(this, duration, camera3D, complete);
        }

        if (duration === 0.0 && defined(this._completeMorph)) {
            this._completeMorph();
        }
    };

    /**
     * Returns true if this object was destroyed; otherwise, false.
     * <br /><br />
     * If this object was destroyed, it should not be used; calling any function other than
     * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
     *
     * @returns {Boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
     */
    SceneTransitioner.prototype.isDestroyed = function() {
        return false;
    };

    /**
     * Once an object is destroyed, it should not be used; calling any function other than
     * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.  Therefore,
     * assign the return value (<code>undefined</code>) to the object as done in the example.
     *
     * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
     *
     * @example
     * transitioner = transitioner && transitioner.destroy();
     */
    SceneTransitioner.prototype.destroy = function() {
        destroyMorphHandler(this);
        return destroyObject(this);
    };

    function createMorphHandler(transitioner, completeMorphFunction) {
        if (transitioner._scene.completeMorphOnUserInput) {
            transitioner._morphHandler = new ScreenSpaceEventHandler(transitioner._scene.canvas, false);

            var completeMorph = function() {
                transitioner._morphCancelled = true;
                completeMorphFunction(transitioner);
            };
            transitioner._completeMorph = completeMorph;
            transitioner._morphHandler.setInputAction(completeMorph, ScreenSpaceEventType.LEFT_DOWN);
            transitioner._morphHandler.setInputAction(completeMorph, ScreenSpaceEventType.MIDDLE_DOWN);
            transitioner._morphHandler.setInputAction(completeMorph, ScreenSpaceEventType.RIGHT_DOWN);
            transitioner._morphHandler.setInputAction(completeMorph, ScreenSpaceEventType.WHEEL);
        }
    }

    function destroyMorphHandler(transitioner) {
        var tweens = transitioner._currentTweens;
        for ( var i = 0; i < tweens.length; ++i) {
            tweens[i].cancelTween();
        }
        transitioner._currentTweens.length = 0;
        transitioner._morphHandler = transitioner._morphHandler && transitioner._morphHandler.destroy();
    }

    var scratchCVTo3DCartographic = new Cartographic();
    var scratchCVTo3DSurfacePoint = new Cartesian3();
    var scratchCVTo3DFromENU = new Matrix4();
    var scratchCVTo3DCamera = {
        position : new Cartesian3(),
        direction : new Cartesian3(),
        up : new Cartesian3(),
        frustum : undefined
    };

    function getColumbusViewTo3DCamera(transitioner, ellipsoid) {
        var scene = transitioner._scene;
        var camera = scene.camera;

        var camera3D = scratchCVTo3DCamera;
        var position = camera3D.position;
        var direction = camera3D.direction;
        var up = camera3D.up;

        var positionCarto = scene.mapProjection.unproject(camera.position, scratchCVTo3DCartographic);
        ellipsoid.cartographicToCartesian(positionCarto, position);
        var surfacePoint = ellipsoid.scaleToGeodeticSurface(position, scratchCVTo3DSurfacePoint);

        var fromENU = Transforms.eastNorthUpToFixedFrame(surfacePoint, ellipsoid, scratchCVTo3DFromENU);

        Matrix4.multiplyByPointAsVector(fromENU, camera.direction, direction);
        Matrix4.multiplyByPointAsVector(fromENU, camera.up, up);

        return camera3D;
    }

    var scratchCVTo3DStartPos = new Cartesian3();
    var scratchCVTo3DStartDir = new Cartesian3();
    var scratchCVTo3DStartUp = new Cartesian3();
    var scratchCVTo3DEndPos = new Cartesian3();
    var scratchCVTo3DEndDir = new Cartesian3();
    var scratchCVTo3DEndUp = new Cartesian3();

    function morphFromColumbusViewTo3D(transitioner, duration, endCamera, complete) {
        duration *= 0.5;

        var scene = transitioner._scene;
        var camera = scene.camera;

        var startPos = Cartesian3.clone(camera.position, scratchCVTo3DStartPos);
        var startDir = Cartesian3.clone(camera.direction, scratchCVTo3DStartDir);
        var startUp = Cartesian3.clone(camera.up, scratchCVTo3DStartUp);

        var endPos = Matrix4.multiplyByPoint(Camera.TRANSFORM_2D_INVERSE, endCamera.position, scratchCVTo3DEndPos);
        var endDir = Matrix4.multiplyByPointAsVector(Camera.TRANSFORM_2D_INVERSE, endCamera.direction, scratchCVTo3DEndDir);
        var endUp = Matrix4.multiplyByPointAsVector(Camera.TRANSFORM_2D_INVERSE, endCamera.up, scratchCVTo3DEndUp);

        function update(value) {
            columbusViewMorph(startPos, endPos, value.time, camera.position);
            columbusViewMorph(startDir, endDir, value.time, camera.direction);
            columbusViewMorph(startUp, endUp, value.time, camera.up);
            Cartesian3.cross(camera.direction, camera.up, camera.right);
            Cartesian3.normalize(camera.right, camera.right);
        }

        var tween = scene.tweens.add({
            duration : duration,
            easingFunction : EasingFunction.QUARTIC_OUT,
            startObject : {
                time : 0.0
            },
            stopObject : {
                time : 1.0
            },
            update : update,
            complete : function() {
                addMorphTimeAnimations(transitioner, scene, 0.0, 1.0, duration, complete);
            }
        });
        transitioner._currentTweens.push(tween);
    }

    var scratch2DTo3DFrustum = new PerspectiveFrustum();

    function morphFrom2DTo3D(transitioner, duration, ellipsoid) {
        duration /= 3.0;

        var scene = transitioner._scene;
        var camera = scene.camera;
        var frustum = scratch2DTo3DFrustum;
        frustum.aspectRatio = scene.drawingBufferWidth / scene.drawingBufferHeight;
        frustum.fov = CesiumMath.toRadians(60.0);

        var camera3D;
        if (duration > 0.0) {
            camera3D = scratchCVTo3DCamera;
            Cartesian3.fromDegrees(0.0, 0.0, 5.0 * ellipsoid.maximumRadius, ellipsoid, camera3D.position);
            Cartesian3.negate(camera3D.position, camera3D.direction);
            Cartesian3.normalize(camera3D.direction, camera3D.direction);
            Cartesian3.clone(Cartesian3.UNIT_Z, camera3D.up);
        } else {
            camera.position.z = camera.frustum.right - camera.frustum.left;

            camera3D = getColumbusViewTo3DCamera(transitioner, ellipsoid);
        }

        camera3D.frustum = frustum;

        var complete = complete3DCallback(camera3D);
        createMorphHandler(transitioner, complete);

        var startPos = Cartesian3.clone(camera.position, scratch3DToCVStartPos);
        var startDir = Cartesian3.clone(camera.direction, scratch3DToCVStartDir);
        var startUp = Cartesian3.clone(camera.up, scratch3DToCVStartUp);

        var endPos = Cartesian3.fromElements(0.0, 0.0, 5.0 * ellipsoid.maximumRadius, scratch3DToCVEndPos);
        var endDir = Cartesian3.negate(Cartesian3.UNIT_Z, scratch3DToCVEndDir);
        var endUp = Cartesian3.clone(Cartesian3.UNIT_Y, scratch3DToCVEndUp);

        var startRight = camera.frustum.right;
        var endRight = endPos.z * 0.5;

        function update(value) {
            columbusViewMorph(startPos, endPos, value.time, camera.position);
            columbusViewMorph(startDir, endDir, value.time, camera.direction);
            columbusViewMorph(startUp, endUp, value.time, camera.up);
            Cartesian3.cross(camera.direction, camera.up, camera.right);
            Cartesian3.normalize(camera.right, camera.right);

            var frustum = camera.frustum;
            frustum.right = CesiumMath.lerp(startRight, endRight, value.time);
            frustum.left = -frustum.right;
            frustum.top = frustum.right * (scene.drawingBufferHeight / scene.drawingBufferWidth);
            frustum.bottom = -frustum.top;

            camera.position.z = 2.0 * scene.mapProjection.ellipsoid.maximumRadius;
        }

        if (duration > 0.0) {
            var tween = scene.tweens.add({
                duration : duration,
                easingFunction : EasingFunction.QUARTIC_OUT,
                startObject : {
                    time : 0.0
                },
                stopObject : {
                    time : 1.0
                },
                update : update,
                complete : function() {
                    scene._mode = SceneMode.MORPHING;
                    morphOrthographicToPerspective(transitioner, duration, camera3D, function() {
                        morphFromColumbusViewTo3D(transitioner, duration, camera3D, complete);
                    });
                }
            });
            transitioner._currentTweens.push(tween);
        } else {
            morphOrthographicToPerspective(transitioner, duration, camera3D, function() {
                morphFromColumbusViewTo3D(transitioner, duration, camera3D, complete);
            });
        }
    }

    function columbusViewMorph(startPosition, endPosition, time, result) {
        // Just linear for now.
        return Cartesian3.lerp(startPosition, endPosition, time, result);
    }

    function morphPerspectiveToOrthographic(transitioner, duration, endCamera, updateHeight, complete) {
        var scene = transitioner._scene;
        var camera = scene.camera;

        var startFOV = camera.frustum.fov;
        var endFOV = CesiumMath.RADIANS_PER_DEGREE * 0.5;
        var d = endCamera.position.z * Math.tan(startFOV * 0.5);
        camera.frustum.far = d / Math.tan(endFOV * 0.5) + 10000000.0;

        function update(value) {
            camera.frustum.fov = CesiumMath.lerp(startFOV, endFOV, value.time);
            var height = d / Math.tan(camera.frustum.fov * 0.5);
            updateHeight(camera, height);
        }
        var tween = scene.tweens.add({
            duration : duration,
            easingFunction : EasingFunction.QUARTIC_OUT,
            startObject : {
                time : 0.0
            },
            stopObject : {
                time : 1.0
            },
            update : update,
            complete : function() {
                camera.frustum = endCamera.frustum.clone();
                complete(transitioner);
            }
        });
        transitioner._currentTweens.push(tween);
    }

    var scratchCVTo2DStartPos = new Cartesian3();
    var scratchCVTo2DStartDir = new Cartesian3();
    var scratchCVTo2DStartUp = new Cartesian3();
    var scratchCVTo2DEndPos = new Cartesian3();
    var scratchCVTo2DEndDir = new Cartesian3();
    var scratchCVTo2DEndUp = new Cartesian3();
    var scratchCVTo2DFrustum = new OrthographicFrustum();
    var scratchCVTo2DRay = new Ray();
    var scratchCVTo2DPickPos = new Cartesian3();
    var scratchCVTo2DCamera = {
        position : undefined,
        direction : undefined,
        up : undefined,
        frustum : undefined
    };

    function morphFromColumbusViewTo2D(transitioner, duration) {
        duration *= 0.5;

        var scene = transitioner._scene;
        var camera = scene.camera;

        var startPos = Cartesian3.clone(camera.position, scratchCVTo2DStartPos);
        var startDir = Cartesian3.clone(camera.direction, scratchCVTo2DStartDir);
        var startUp = Cartesian3.clone(camera.up, scratchCVTo2DStartUp);

        var endDir = Cartesian3.negate(Cartesian3.UNIT_Z, scratchCVTo2DEndDir);
        var endUp = Cartesian3.clone(Cartesian3.UNIT_Y, scratchCVTo2DEndUp);

        var endPos = scratchCVTo2DEndPos;

        if (duration > 0.0) {
            Cartesian3.clone(Cartesian3.ZERO, scratchCVTo2DEndPos);
            endPos.z = 5.0 * scene.mapProjection.ellipsoid.maximumRadius;
        } else {
            Cartesian3.clone(startPos, scratchCVTo2DEndPos);

            var ray = scratchCVTo2DRay;
            Matrix4.multiplyByPoint(Camera.TRANSFORM_2D, startPos, ray.origin);
            Matrix4.multiplyByPointAsVector(Camera.TRANSFORM_2D, startDir, ray.direction);

            var globe = scene.globe;
            if (defined(globe)) {
                var pickPos = globe.pick(ray, scene, scratchCVTo2DPickPos);
                if (defined(pickPos)) {
                    Matrix4.multiplyByPoint(Camera.TRANSFORM_2D_INVERSE, pickPos, endPos);
                    endPos.z += Cartesian3.distance(startPos, endPos);
                }
            }
        }

        var frustum = scratchCVTo2DFrustum;
        frustum.right = endPos.z * 0.5;
        frustum.left = -frustum.right;
        frustum.top = frustum.right * (scene.drawingBufferHeight / scene.drawingBufferWidth);
        frustum.bottom = -frustum.top;

        var camera2D = scratchCVTo2DCamera;
        camera2D.position = endPos;
        camera2D.direction = endDir;
        camera2D.up = endUp;
        camera2D.frustum = frustum;

        var complete = complete2DCallback(camera2D);
        createMorphHandler(transitioner, complete);

        function updateCV(value) {
            columbusViewMorph(startPos, endPos, value.time, camera.position);
            columbusViewMorph(startDir, endDir, value.time, camera.direction);
            columbusViewMorph(startUp, endUp, value.time, camera.up);
            Cartesian3.cross(camera.direction, camera.up, camera.right);
            Cartesian3.normalize(camera.right, camera.right);
        }

        function updateHeight(camera, height) {
            camera.position.z = height;
        }

        var tween = scene.tweens.add({
            duration : duration,
            easingFunction : EasingFunction.QUARTIC_OUT,
            startObject : {
                time : 0.0
            },
            stopObject : {
                time : 1.0
            },
            update : updateCV,
            complete : function() {
                morphPerspectiveToOrthographic(transitioner, duration, camera2D, updateHeight, complete);
            }
        });
        transitioner._currentTweens.push(tween);
    }

    var scratch3DTo2DCartographic = new Cartographic();
    var scratch3DTo2DCamera = {
        position : new Cartesian3(),
        direction : new Cartesian3(),
        up : new Cartesian3(),
        position2D : new Cartesian3(),
        direction2D : new Cartesian3(),
        up2D : new Cartesian3(),
        frustum : new OrthographicFrustum()
    };
    var scratch3DTo2DEndCamera = {
        position : new Cartesian3(),
        direction : new Cartesian3(),
        up : new Cartesian3(),
        frustum : undefined
    };
    var scratch3DTo2DPickPosition = new Cartesian3();
    var scratch3DTo2DRay = new Ray();
    var scratch3DTo2DToENU = new Matrix4();
    var scratch3DTo2DSurfacePoint = new Cartesian3();

    function morphFrom3DTo2D(transitioner, duration, ellipsoid) {
        duration *= 0.5;

        var scene = transitioner._scene;
        var camera = scene.camera;
        var camera2D = scratch3DTo2DCamera;

        if (duration > 0.0) {
            Cartesian3.clone(Cartesian3.ZERO, camera2D.position);
            camera2D.position.z = 5.0 * ellipsoid.maximumRadius;
            Cartesian3.negate(Cartesian3.UNIT_Z, camera2D.direction);
            Cartesian3.clone(Cartesian3.UNIT_Y, camera2D.up);
        } else {
            ellipsoid.cartesianToCartographic(camera.positionWC, scratch3DTo2DCartographic);
            scene.mapProjection.project(scratch3DTo2DCartographic, camera2D.position);

            Cartesian3.negate(Cartesian3.UNIT_Z, camera2D.direction);
            Cartesian3.clone(Cartesian3.UNIT_Y, camera2D.up);

            var ray = scratch3DTo2DRay;
            Cartesian3.clone(camera2D.position2D, ray.origin);
            var rayDirection = Cartesian3.clone(camera.directionWC, ray.direction);
            var surfacePoint = ellipsoid.scaleToGeodeticSurface(camera.positionWC, scratch3DTo2DSurfacePoint);
            var toENU = Transforms.eastNorthUpToFixedFrame(surfacePoint, ellipsoid, scratch3DTo2DToENU);
            Matrix4.inverseTransformation(toENU, toENU);
            Matrix4.multiplyByPointAsVector(toENU, rayDirection, rayDirection);
            Matrix4.multiplyByPointAsVector(Camera.TRANSFORM_2D, rayDirection, rayDirection);

            var globe = scene.globe;
            if (defined(globe)) {
                var pickedPos = globe.pick(ray, scene, scratch3DTo2DPickPosition);
                if (defined(pickedPos)) {
                    var height = Cartesian3.distance(camera2D.position2D, pickedPos);
                    pickedPos.x += height;
                    Cartesian3.clone(pickedPos, camera2D.position2D);
                }
            }
        }

        function updateHeight(camera, height) {
            camera.position.x = height;
        }

        Matrix4.multiplyByPoint(Camera.TRANSFORM_2D, camera2D.position, camera2D.position2D);
        Matrix4.multiplyByPointAsVector(Camera.TRANSFORM_2D, camera2D.direction, camera2D.direction2D);
        Matrix4.multiplyByPointAsVector(Camera.TRANSFORM_2D, camera2D.up, camera2D.up2D);

        var frustum = camera2D.frustum;
        frustum.right = camera2D.position.z * 0.5;
        frustum.left = -frustum.right;
        frustum.top = frustum.right * (scene.drawingBufferHeight / scene.drawingBufferWidth);
        frustum.bottom = -frustum.top;

        var endCamera = scratch3DTo2DEndCamera;
        Matrix4.multiplyByPoint(Camera.TRANSFORM_2D_INVERSE, camera2D.position2D, endCamera.position);
        Cartesian3.clone(camera2D.direction, endCamera.direction);
        Cartesian3.clone(camera2D.up, endCamera.up);
        endCamera.frustum = frustum;

        var complete = complete2DCallback(endCamera);
        createMorphHandler(transitioner, complete);

        function completeCallback() {
            morphPerspectiveToOrthographic(transitioner, duration, camera2D, updateHeight, complete);
        }
        morphFrom3DToColumbusView(transitioner, duration, camera2D, completeCallback);
    }

    function morphOrthographicToPerspective(transitioner, duration, cameraCV, complete) {
        var scene = transitioner._scene;
        var camera = scene.camera;

        var height = camera.frustum.right - camera.frustum.left;
        camera.frustum = cameraCV.frustum.clone();

        var endFOV = camera.frustum.fov;
        var startFOV = CesiumMath.RADIANS_PER_DEGREE * 0.5;
        var d = height * Math.tan(endFOV * 0.5);
        camera.frustum.far = d / Math.tan(startFOV * 0.5) + 10000000.0;
        camera.frustum.fov = startFOV;

        function update(value) {
            camera.frustum.fov = CesiumMath.lerp(startFOV, endFOV, value.time);
            camera.position.z = d / Math.tan(camera.frustum.fov * 0.5);
        }
        var tween = scene.tweens.add({
            duration : duration,
            easingFunction : EasingFunction.QUARTIC_OUT,
            startObject : {
                time : 0.0
            },
            stopObject : {
                time : 1.0
            },
            update : update,
            complete : function() {
                complete(transitioner);
            }
        });
        transitioner._currentTweens.push(tween);
    }

    function morphFrom2DToColumbusView(transitioner, duration, cameraCV, complete) {
        duration *= 0.5;

        var scene = transitioner._scene;
        var camera = scene.camera;

        var startPos = Cartesian3.clone(camera.position, scratch3DToCVStartPos);
        var startDir = Cartesian3.clone(camera.direction, scratch3DToCVStartDir);
        var startUp = Cartesian3.clone(camera.up, scratch3DToCVStartUp);

        var endPos = Cartesian3.clone(cameraCV.position, scratch3DToCVEndPos);
        var endDir = Cartesian3.clone(cameraCV.direction, scratch3DToCVEndDir);
        var endUp = Cartesian3.clone(cameraCV.up, scratch3DToCVEndUp);

        var startRight = camera.frustum.right;
        var endRight = endPos.z * 0.5;

        function update(value) {
            columbusViewMorph(startPos, endPos, value.time, camera.position);
            columbusViewMorph(startDir, endDir, value.time, camera.direction);
            columbusViewMorph(startUp, endUp, value.time, camera.up);
            Cartesian3.cross(camera.direction, camera.up, camera.right);
            Cartesian3.normalize(camera.right, camera.right);

            var frustum = camera.frustum;
            frustum.right = CesiumMath.lerp(startRight, endRight, value.time);
            frustum.left = -frustum.right;
            frustum.top = frustum.right * (scene.drawingBufferHeight / scene.drawingBufferWidth);
            frustum.bottom = -frustum.top;

            camera.position.z = 2.0 * scene.mapProjection.ellipsoid.maximumRadius;
        }
        var tween = scene.tweens.add({
            duration : duration,
            easingFunction : EasingFunction.QUARTIC_OUT,
            startObject : {
                time : 0.0
            },
            stopObject : {
                time : 1.0
            },
            update : update,
            complete : function() {
                scene._mode = SceneMode.MORPHING;
                morphOrthographicToPerspective(transitioner, duration, cameraCV, complete);
            }
        });
        transitioner._currentTweens.push(tween);
    }

    var scratch3DToCVStartPos = new Cartesian3();
    var scratch3DToCVStartDir = new Cartesian3();
    var scratch3DToCVStartUp = new Cartesian3();
    var scratch3DToCVEndPos = new Cartesian3();
    var scratch3DToCVEndDir = new Cartesian3();
    var scratch3DToCVEndUp = new Cartesian3();

    function morphFrom3DToColumbusView(transitioner, duration, endCamera, complete) {
        var scene = transitioner._scene;
        var camera = scene.camera;

        var startPos = Cartesian3.clone(camera.position, scratch3DToCVStartPos);
        var startDir = Cartesian3.clone(camera.direction, scratch3DToCVStartDir);
        var startUp = Cartesian3.clone(camera.up, scratch3DToCVStartUp);

        var endPos = Cartesian3.clone(endCamera.position2D, scratch3DToCVEndPos);
        var endDir = Cartesian3.clone(endCamera.direction2D, scratch3DToCVEndDir);
        var endUp = Cartesian3.clone(endCamera.up2D, scratch3DToCVEndUp);

        function update(value) {
            columbusViewMorph(startPos, endPos, value.time, camera.position);
            columbusViewMorph(startDir, endDir, value.time, camera.direction);
            columbusViewMorph(startUp, endUp, value.time, camera.up);
            Cartesian3.cross(camera.direction, camera.up, camera.right);
            Cartesian3.normalize(camera.right, camera.right);
        }
        var tween = scene.tweens.add({
            duration : duration,
            easingFunction : EasingFunction.QUARTIC_OUT,
            startObject : {
                time : 0.0
            },
            stopObject : {
                time : 1.0
            },
            update : update,
            complete : function() {
                addMorphTimeAnimations(transitioner, scene, 1.0, 0.0, duration, complete);
            }
        });
        transitioner._currentTweens.push(tween);
    }

    function addMorphTimeAnimations(transitioner, scene, start, stop, duration, complete) {
        // Later, this will be linear and each object will adjust, if desired, in its vertex shader.
        var options = {
            object : scene,
            property : 'morphTime',
            startValue : start,
            stopValue : stop,
            duration : duration,
            easingFunction : EasingFunction.QUARTIC_OUT
        };

        if (defined(complete)) {
            options.complete = function() {
                complete(transitioner);
            };
        }

        var tween = scene.tweens.addProperty(options);
        transitioner._currentTweens.push(tween);
    }

    function complete3DCallback(camera3D) {
        return function(transitioner) {
            var scene = transitioner._scene;
            scene._mode = SceneMode.SCENE3D;
            scene.morphTime = SceneMode.getMorphTime(SceneMode.SCENE3D);

            destroyMorphHandler(transitioner);

            if (transitioner._previousMode !== SceneMode.MORPHING || transitioner._morphCancelled) {
                transitioner._morphCancelled = false;

                var camera = scene.camera;
                Cartesian3.clone(camera3D.position, camera.position);
                Cartesian3.clone(camera3D.direction, camera.direction);
                Cartesian3.clone(camera3D.up, camera.up);
                Cartesian3.cross(camera.direction, camera.up, camera.right);
                Cartesian3.normalize(camera.right, camera.right);

                if (defined(camera3D.frustum)) {
                    camera.frustum = camera3D.frustum.clone();
                }
            }

            var wasMorphing = defined(transitioner._completeMorph);
            transitioner._completeMorph = undefined;
            scene.camera.update(scene.mode);
            transitioner._scene.morphComplete.raiseEvent(transitioner, transitioner._previousMode, SceneMode.SCENE3D, wasMorphing);
        };
    }

    function complete2DCallback(camera2D) {
        return function(transitioner) {
            var scene = transitioner._scene;

            scene._mode = SceneMode.SCENE2D;
            scene.morphTime = SceneMode.getMorphTime(SceneMode.SCENE2D);

            destroyMorphHandler(transitioner);

            var camera = scene.camera;
            Cartesian3.clone(camera2D.position, camera.position);
            camera.position.z = scene.mapProjection.ellipsoid.maximumRadius * 2.0;
            Cartesian3.clone(camera2D.direction, camera.direction);
            Cartesian3.clone(camera2D.up, camera.up);
            Cartesian3.cross(camera.direction, camera.up, camera.right);
            Cartesian3.normalize(camera.right, camera.right);
            camera.frustum = camera2D.frustum.clone();

            var wasMorphing = defined(transitioner._completeMorph);
            transitioner._completeMorph = undefined;
            scene.camera.update(scene.mode);
            transitioner._scene.morphComplete.raiseEvent(transitioner, transitioner._previousMode, SceneMode.SCENE2D, wasMorphing);
        };
    }

    function completeColumbusViewCallback(cameraCV) {
        return function(transitioner) {
            var scene = transitioner._scene;
            scene._mode = SceneMode.COLUMBUS_VIEW;
            scene.morphTime = SceneMode.getMorphTime(SceneMode.COLUMBUS_VIEW);

            destroyMorphHandler(transitioner);
            scene.camera.frustum = cameraCV.frustum.clone();

            if (transitioner._previousModeMode !== SceneMode.MORPHING || transitioner._morphCancelled) {
                transitioner._morphCancelled = false;

                var camera = scene.camera;
                Cartesian3.clone(cameraCV.position, camera.position);
                Cartesian3.clone(cameraCV.direction, camera.direction);
                Cartesian3.clone(cameraCV.up, camera.up);
                Cartesian3.cross(camera.direction, camera.up, camera.right);
                Cartesian3.normalize(camera.right, camera.right);
            }

            var wasMorphing = defined(transitioner._completeMorph);
            transitioner._completeMorph = undefined;
            scene.camera.update(scene.mode);
            transitioner._scene.morphComplete.raiseEvent(transitioner, transitioner._previousMode, SceneMode.COLUMBUS_VIEW, wasMorphing);
        };
    }

    return SceneTransitioner;
});
