/*global define*/
define([
        '../Core/destroyObject',
        '../Core/DeveloperError',
        '../Core/Math',
        '../Core/Event',
        '../Core/ScreenSpaceEventHandler',
        '../Core/ScreenSpaceEventType',
        '../Core/Ellipsoid',
        '../Core/Cartesian2',
        '../Core/Cartesian3',
        '../Core/Cartesian4',
        '../Core/Cartographic',
        '../Core/Matrix4',
        '../ThirdParty/Tween',
        './OrthographicFrustum',
        './PerspectiveFrustum',
        './SceneMode'
    ], function(
        destroyObject,
        DeveloperError,
        CesiumMath,
        Event,
        ScreenSpaceEventHandler,
        ScreenSpaceEventType,
        Ellipsoid,
        Cartesian2,
        Cartesian3,
        Cartesian4,
        Cartographic,
        Matrix4,
        Tween,
        OrthographicFrustum,
        PerspectiveFrustum,
        SceneMode) {
    "use strict";

    /**
     * Transitions the scene among available modes. The transitions can
     * either be instantaneous or animated.
     * @alias SceneTransitioner
     * @constructor
     *
     * @param {Scene} scene The scene to be transitioned.
     * @param {Ellipsoid} [ellipsoid=Ellipsoid.WGS84] The ellipsoid to be transitioned.
     *
     * @exception {DeveloperError} scene is required.
     *
     * @see Scene
     * @see SceneMode
     */
    var SceneTransitioner = function(scene, ellipsoid) {
        if (typeof scene === 'undefined') {
            throw new DeveloperError('scene is required.');
        }

        /**
         * Gets or sets the amount of time, in milliseconds, for
         * transition animations to complete.
         * @type {Number}
         */
        this.morphDuration = 2000;

        /**
         * Gets or sets whether or not to instantly complete the
         * transition animation on user input.
         * @type {Boolean}
         */
        this.completeMorphOnUserInput = true;

        /**
         * Gets the event fired at the beginning of a transition.
         * @type {Event}
         */
        this.onTransitionStart = new Event();

        /**
         * Gets the event fired at the completion of a transition.
         * @type {Event}
         */
        this.onTransitionComplete = new Event();

        this._scene = scene;
        this._ellipsoid = ellipsoid || Ellipsoid.WGS84;
        var canvas = scene.getCanvas();

        // Position camera and size frustum so the entire 2D map is visible
        var maxRadii = this._ellipsoid.getMaximumRadius();
        var position = new Cartesian3(0.0, 0.0, 2.0 * maxRadii);
        var direction = position.negate().normalize();
        var up = Cartesian3.UNIT_Y;

        var frustum = new OrthographicFrustum();
        frustum.right = maxRadii * Math.PI;
        frustum.left = -frustum.right;
        frustum.top = frustum.right * (canvas.clientHeight / canvas.clientWidth);
        frustum.bottom = -frustum.top;

        var transform = new Matrix4(0.0, 0.0, 1.0, 0.0, //
                                    1.0, 0.0, 0.0, 0.0, //
                                    0.0, 1.0, 0.0, 0.0, //
                                    0.0, 0.0, 0.0, 1.0);

        this._camera2D = {
            position : position,
            direction : direction,
            up : up,
            frustum : frustum,
            transform : transform
        };

        position = new Cartesian3(0.0, -1.0, 1.0).normalize().multiplyByScalar(5.0 * maxRadii);
        direction = Cartesian3.ZERO.subtract(position).normalize();
        var right = direction.cross(Cartesian3.UNIT_Z).normalize();
        up = right.cross(direction);

        frustum = new PerspectiveFrustum();
        frustum.fovy = CesiumMath.toRadians(60.0);
        frustum.aspectRatio = canvas.clientWidth / canvas.clientHeight;

        this._cameraCV = {
            position : position,
            direction : direction,
            up : up,
            frustum : frustum,
            transform : transform
        };

        position = new Cartesian3(0.0, -2.0, 1.0).normalize().multiplyByScalar(2.0 * maxRadii);
        direction = Cartesian3.ZERO.subtract(position).normalize();
        right = direction.cross(Cartesian3.UNIT_Z).normalize();
        up = right.cross(direction);

        this._camera3D = {
            position : position,
            direction : direction,
            up : up,
            frustum : frustum
        };

        this._currentAnimations = [];
        this._morphHandler = undefined;
        this._morphCancelled = false;
        this._completeMorph = undefined;
    };

    /**
     * @memberof SceneTransitioner
     * @returns {Scene} The scene to be transitioned.
     */
    SceneTransitioner.prototype.getScene = function() {
        return this._scene;
    };

    /**
     * @memberof SceneTransitioner
     * @returns {Ellipsoid} The ellipsoid to be transitioned.
     */
    SceneTransitioner.prototype.getEllipsoid = function() {
        return this._ellipsoid;
    };

    /**
     * Instantly transitions the scene to 2D.
     * @memberof SceneTransitioner
     */
    SceneTransitioner.prototype.to2D = function() {
        if (typeof this._completeMorph !== 'undefined') {
            this._completeMorph();
        }

        this._previousMode = this._scene.mode;
        if (this._previousMode !== SceneMode.SCENE2D) {
            this.onTransitionStart.raiseEvent(this, this._previousMode, SceneMode.SCENE2D, false);
            complete2DCallback(this);
        }
    };

    /**
     * Instantly transitions the scene to Columbus View.
     * @memberof SceneTransitioner
     */
    SceneTransitioner.prototype.toColumbusView = function() {
        if (typeof this._completeMorph !== 'undefined') {
            this._completeMorph();
        }

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

        if (this._previousMode !== SceneMode.COLUMBUS_VIEW) {
            this.onTransitionStart.raiseEvent(this, this._previousMode, SceneMode.COLUMBUS_VIEW, false);
            completeColumbusViewCallback(this);
        }
    };

    /**
     * Instantly transitions the scene to 3D.
     * @memberof SceneTransitioner
     */
    SceneTransitioner.prototype.to3D = function() {
        if (typeof this._completeMorph !== 'undefined') {
            this._completeMorph();
        }

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

        if (scene.mode !== SceneMode.SCENE3D) {
            this.onTransitionStart.raiseEvent(this, this._previousMode, SceneMode.SCENE3D, false);
            complete3DCallback(this);
        }
    };

    /**
     * Instantly completes an active transition.
     * @memberof SceneTransitioner
     *
     * @exception {DeveloperError} completeMorph can only be called during a transition.
     */
    SceneTransitioner.prototype.completeMorph = function() {
        if (typeof this._completeMorph === 'undefined') {
            throw new DeveloperError('completeMorph can only be called while morphing');
        }
        this._completeMorph();
    };

    /**
     * Asynchronously transitions the scene to 2D.
     * @memberof SceneTransitioner
     */
    SceneTransitioner.prototype.morphTo2D = function() {
        if (typeof this._completeMorph !== 'undefined') {
            this._completeMorph();
        }

        var currentMode = this._scene.mode;
        if (currentMode === SceneMode.SCENE2D || currentMode === SceneMode.MORPHING) {
            return;
        }
        this.onTransitionStart.raiseEvent(this, currentMode, SceneMode.SCENE2D, true);
        this._previousMode = SceneMode.MORPHING;

        updateFrustums(this);
        this._scene.mode = SceneMode.MORPHING;
        createMorphHandler(this, complete2DCallback);

        if (currentMode === SceneMode.COLUMBUS_VIEW) {
            morphFromColumbusViewTo2D(this, this.morphDuration, complete2DCallback);
        } else {
            morphFrom3DTo2D(this, this.morphDuration, complete2DCallback);
        }
    };

    /**
     * Asynchronously transitions the scene to Columbus View.
     * @memberof SceneTransitioner
     */
    SceneTransitioner.prototype.morphToColumbusView = function() {
        if (typeof this._completeMorph !== 'undefined') {
            this._completeMorph();
        }

        var currentMode = this._scene.mode;
        if (currentMode === SceneMode.COLUMBUS_VIEW || currentMode === SceneMode.MORPHING) {
            return;
        }
        this.onTransitionStart.raiseEvent(this, currentMode, SceneMode.COLUMBUS_VIEW, true);
        this._previousMode = SceneMode.MORPHING;

        updateFrustums(this);
        this._scene.mode = SceneMode.MORPHING;
        createMorphHandler(this, completeColumbusViewCallback);

        if (currentMode === SceneMode.SCENE2D) {
            morphFrom2DToColumbusView(this, this.morphDuration, completeColumbusViewCallback);
        } else {
            morphFrom3DToColumbusView(this, this.morphDuration, this._cameraCV, completeColumbusViewCallback);
        }
    };

    /**
     * Asynchronously transitions the scene to 3D.
     * @memberof SceneTransitioner
     */
    SceneTransitioner.prototype.morphTo3D = function() {
        if (typeof this._completeMorph !== 'undefined') {
            this._completeMorph();
        }

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

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

        updateFrustums(this);
        scene.mode = SceneMode.MORPHING;
        createMorphHandler(this, complete3DCallback);

        if (this._previousMode === SceneMode.SCENE2D) {
            morphFrom2DTo3D(this, this.morphDuration, complete3DCallback);
        } else {
            morphFromColumbusViewTo3D(this, this.morphDuration, complete3DCallback);
        }
    };

    /**
     * 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.
     * @memberof SceneTransitioner
     *
     * @return {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.
     * @memberof SceneTransitioner
     *
     * @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 setCameraTransform(camera, transform) {
        var pos = new Cartesian4(camera.position.x, camera.position.y, camera.position.z, 1.0);
        var dir = new Cartesian4(camera.direction.x, camera.direction.y, camera.direction.z, 0.0);
        var up = new Cartesian4(camera.up.x, camera.up.y, camera.up.z, 0.0);

        var frame = transform.inverseTransformation().multiply(camera.transform);
        camera.transform = transform.clone();

        camera.position = Cartesian3.fromCartesian4(frame.multiplyByVector(pos));
        camera.direction = Cartesian3.fromCartesian4(frame.multiplyByVector(dir));
        camera.up = Cartesian3.fromCartesian4(frame.multiplyByVector(up));
        camera.right = camera.direction.cross(camera.up);
    }

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

            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 animations = transitioner._scene.getAnimations();
        for ( var i = 0; i < transitioner._currentAnimations.length; ++i) {
            animations.remove(transitioner._currentAnimations[i]);
        }
        transitioner._currentAnimations.length = 0;
        transitioner._morphHandler = transitioner._morphHandler && transitioner._morphHandler.destroy();
    }

    function morphFromColumbusViewTo3D(transitioner, duration, onComplete) {
        var scene = transitioner._scene;

        var camera = scene.getCamera();
        setCameraTransform(camera, Matrix4.IDENTITY);

        var startPos = camera.position;
        var startDir = camera.direction;
        var startUp = camera.up;

        var maxRadii = transitioner._ellipsoid.getMaximumRadius();
        var endPos = transitioner._ellipsoid.cartographicToCartesian(new Cartographic(0.0, 0.0, 10.0));
        endPos = endPos.normalize().multiplyByScalar(2.0 * maxRadii);
        var endDir = Cartesian3.ZERO.subtract(endPos).normalize();
        var endRight = endDir.cross(Cartesian3.UNIT_Z).normalize();
        var endUp = endRight.cross(endDir);

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

        var animation = scene.getAnimations().add({
            duration : duration,
            easingFunction : Tween.Easing.Quartic.Out,
            startValue : {
                time : 0.0
            },
            stopValue : {
                time : 1.0
            },
            onUpdate : update
        });
        transitioner._currentAnimations.push(animation);

        addMorphTimeAnimations(transitioner, scene, 0.0, 1.0, duration, onComplete);
    }

    function morphFrom2DTo3D(transitioner, duration, onComplete) {
        duration = duration * 0.5;

        var camera = transitioner._scene.getCamera();

        morphOrthographicToPerspective(transitioner, duration, function() {
            camera.frustum = transitioner._cameraCV.frustum.clone();
            camera.transform = transitioner._cameraCV.transform.clone();
            morphFromColumbusViewTo3D(transitioner, duration, onComplete);
        });
    }

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

    function morphPerspectiveToOrthographic(transitioner, duration, onComplete) {
        var scene = transitioner._scene;
        var camera = scene.getCamera();

        var startPos = camera.position;
        var startFOVy = camera.frustum.fovy;
        var endFOVy = CesiumMath.RADIANS_PER_DEGREE * 0.5;
        var d = startPos.magnitude() * Math.tan(startFOVy * 0.5);
        camera.frustum.far = d / Math.tan(endFOVy * 0.5) + 10000000.0;

        var update = function(value) {
            camera.frustum.fovy = CesiumMath.lerp(startFOVy, endFOVy, value.time);

            var distance = d / Math.tan(camera.frustum.fovy * 0.5);
            camera.position = camera.position.normalize().multiplyByScalar(distance);
        };

        var animation = scene.getAnimations().add({
            duration : duration,
            easingFunction : Tween.Easing.Quartic.Out,
            startValue : {
                time : 0.0
            },
            stopValue : {
                time : 1.0
            },
            onUpdate : update,
            onComplete : function() {
                camera.frustum = transitioner._camera2D.frustum.clone();
                onComplete(transitioner);
            }
        });
        transitioner._currentAnimations.push(animation);
    }

    function morphFromColumbusViewTo2D(transitioner, duration, onComplete) {
        var scene = transitioner._scene;
        var camera = scene.getCamera();
        var maxRadii = transitioner._ellipsoid.getMaximumRadius();

        setCameraTransform(camera, transitioner._cameraCV.transform);

        var startPos = camera.position.clone();
        var startDir = camera.direction.clone();
        var startUp = camera.up.clone();

        var tanPhi = Math.tan(transitioner._cameraCV.frustum.fovy * 0.5);
        var tanTheta = transitioner._cameraCV.frustum.aspectRatio * tanPhi;
        var d = (maxRadii * Math.PI) / tanTheta;

        var endPos = transitioner._camera2D.position.normalize().multiplyByScalar(d);
        var endDir = transitioner._camera2D.direction.clone();
        var endUp = transitioner._camera2D.up.clone();

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

        duration = duration * 0.5;
        var animation = scene.getAnimations().add({
            duration : duration,
            easingFunction : Tween.Easing.Quartic.Out,
            startValue : {
                time : 0.0
            },
            stopValue : {
                time : 1.0
            },
            onUpdate : updateCV,
            onComplete : function() {
                morphPerspectiveToOrthographic(transitioner, duration, onComplete);
            }
        });
        transitioner._currentAnimations.push(animation);
    }

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

        var maxRadii = transitioner._ellipsoid.getMaximumRadius();

        var tanPhi = Math.tan(transitioner._camera3D.frustum.fovy * 0.5);
        var tanTheta = transitioner._camera3D.frustum.aspectRatio * tanPhi;
        var d = (maxRadii * Math.PI) / tanTheta;

        var camera3DTo2D = {};
        camera3DTo2D.position = transitioner._camera2D.position.normalize().multiplyByScalar(d);
        camera3DTo2D.direction = transitioner._camera2D.direction.clone();
        camera3DTo2D.up = transitioner._camera2D.up.clone();

        var complete = function() {
            morphPerspectiveToOrthographic(transitioner, duration, onComplete);
        };
        morphFrom3DToColumbusView(transitioner, duration, camera3DTo2D, complete);
    }

    function morphOrthographicToPerspective(transitioner, duration, onComplete) {
        var scene = transitioner._scene;
        var camera = scene.getCamera();
        var maxRadii = transitioner._ellipsoid.getMaximumRadius();

        var tanPhi = Math.tan(transitioner._cameraCV.frustum.fovy * 0.5);
        var tanTheta = transitioner._cameraCV.frustum.aspectRatio * tanPhi;
        var d = (maxRadii * Math.PI) / tanTheta;
        var endPos2D = transitioner._camera2D.position.normalize().multiplyByScalar(d);

        var top = camera.frustum.top;
        var bottom = camera.frustum.bottom;
        var right = camera.frustum.right;
        var left = camera.frustum.left;

        var frustum2D = transitioner._camera2D.frustum;
        var frustumCV = transitioner._cameraCV.frustum;

        var startPos = camera.position.clone();

        var update2D = function(value) {
            camera.position = columbusViewMorph(startPos, endPos2D, value.time);
            camera.frustum.top = CesiumMath.lerp(top, frustum2D.top, value.time);
            camera.frustum.bottom = CesiumMath.lerp(bottom, frustum2D.bottom, value.time);
            camera.frustum.right = CesiumMath.lerp(right, frustum2D.right, value.time);
            camera.frustum.left = CesiumMath.lerp(left, frustum2D.left, value.time);
        };

        var startTime = (right - left) / (2.0 * maxRadii * Math.PI);
        var endTime = 1.0;
        if (startTime > endTime) {
            startTime = 0.0;
        }

        var partialDuration = (endTime - startTime) * duration;
        if (partialDuration < CesiumMath.EPSILON6) {
            if (!startPos.equalsEpsilon(endPos2D, CesiumMath.EPSILON6)) {
                partialDuration = duration;
                startTime = 0.0;
                endTime = 1.0;
            } else {
                // If the camera and frustum are already in position for the switch to
                // a perspective projection, nothing needs to be animated.
                camera.position = endPos2D;
                camera.frustum = frustumCV.clone();
                onComplete(transitioner);
                return;
            }
        }

        var animation = scene.getAnimations().add({
            easingFunction : Tween.Easing.Quartic.Out,
            duration : partialDuration,
            startValue : {
                time : startTime
            },
            stopValue : {
                time : endTime
            },
            onUpdate : update2D,
            onComplete : function() {
                camera.frustum = frustumCV.clone();
                onComplete(transitioner);
            }
        });
        transitioner._currentAnimations.push(animation);
    }

    function morphFrom2DToColumbusView(transitioner, duration, onComplete) {
        var scene = transitioner._scene;
        var camera = scene.getCamera();

        duration = duration * 0.5;

        var completeFrustumChange = function() {
            var startPos = camera.position.clone();
            var startDir = camera.direction.clone();
            var startUp = camera.up.clone();

            var endPos = transitioner._cameraCV.position.clone();
            var endDir = transitioner._cameraCV.direction.clone();
            var endUp = transitioner._cameraCV.up.clone();

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

            var animation = scene.getAnimations().add({
                duration : duration,
                easingFunction : Tween.Easing.Quartic.Out,
                startValue : {
                    time : 0.0
                },
                stopValue : {
                    time : 1.0
                },
                onUpdate : updateCV,
                onComplete : function() {
                    onComplete(transitioner);
                }
            });

            transitioner._currentAnimations.push(animation);
        };

        morphOrthographicToPerspective(transitioner, duration, completeFrustumChange);
    }

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

        var camera = scene.getCamera();
        setCameraTransform(camera, transitioner._cameraCV.transform);

        var startPos = camera.position.clone();
        var startDir = camera.direction.clone();
        var startUp = camera.up.clone();

        var endPos = endCamera.position.clone();
        var endDir = endCamera.direction.clone();
        var endUp = endCamera.up.clone();

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

        var animation = scene.getAnimations().add({
            duration : duration,
            easingFunction : Tween.Easing.Quartic.Out,
            startValue : {
                time : 0.0
            },
            stopValue : {
                time : 1.0
            },
            onUpdate : update,
            onComplete : function() {
                camera.position = endPos;
                camera.direction = endDir;
                camera.up = endUp;
            }
        });
        transitioner._currentAnimations.push(animation);

        addMorphTimeAnimations(transitioner, scene, 1.0, 0.0, duration, onComplete);
    }

    //immediately set the morph time of all objects in the scene
    function setMorphTime(scene, morphTime) {
        scene.morphTime = morphTime;

        var primitives = scene.getPrimitives();
        for ( var i = 0, len = primitives.getLength(); i < len; i++) {
            var primitive = primitives.get(i);
            if (typeof primitive.morphTime !== 'undefined') {
                primitive.morphTime = morphTime;
            }
        }

        var centralBody = primitives.getCentralBody();
        centralBody.morphTime = morphTime;

        if (typeof scene.skyBox !== 'undefined') {
            scene.skyBox.morphTime = morphTime;
        }

        if (typeof scene.skyAtmosphere !== 'undefined') {
            scene.skyAtmosphere.morphTime = morphTime;
        }
    }

    //in the future the animations will be more complicated
    function addMorphTimeAnimations(transitioner, scene, start, stop, duration, onComplete) {
        //for now, all objects morph at the same rate
        var template = {
            duration : duration,
            easingFunction : Tween.Easing.Quartic.Out
        };

        var primitives = scene.getPrimitives();
        var sceneAnimations = scene.getAnimations();
        var animation;
        for ( var i = 0, len = primitives.getLength(); i < len; i++) {
            var primitive = primitives.get(i);
            if (typeof primitive.morphTime !== 'undefined') {
                animation = sceneAnimations.addProperty(primitive, 'morphTime', start, stop, template);
                transitioner._currentAnimations.push(animation);
            }
        }

        var centralBody = primitives.getCentralBody();
        animation = sceneAnimations.addProperty(centralBody, 'morphTime', start, stop, template);
        transitioner._currentAnimations.push(animation);

        if (typeof scene.skyBox !== 'undefined') {
            animation = sceneAnimations.addProperty(scene.skyBox, 'morphTime', start, stop, template);
            transitioner._currentAnimations.push(animation);
        }

        if (typeof scene.skyAtmosphere !== 'undefined') {
            animation = sceneAnimations.addProperty(scene.skyAtmosphere, 'morphTime', start, stop, template);
            transitioner._currentAnimations.push(animation);
        }

        if (typeof onComplete !== 'undefined') {
            template.onComplete = function() {
                onComplete(transitioner);
            };
        }

        animation = sceneAnimations.addProperty(scene, 'morphTime', start, stop, template);
        transitioner._currentAnimations.push(animation);
    }

    function updateFrustums(transitioner) {
        var scene = transitioner._scene;

        var canvas = scene.getCanvas();
        var ratio = canvas.clientHeight / canvas.clientWidth;

        var frustum = transitioner._camera2D.frustum;
        frustum.top = frustum.right * ratio;
        frustum.bottom = -frustum.top;

        ratio = 1.0 / ratio;

        frustum = transitioner._cameraCV.frustum;
        frustum.aspectRatio = ratio;

        frustum = transitioner._camera3D.frustum;
        frustum.aspectRatio = ratio;

        var camera = scene.getCamera();
        switch (scene.mode) {
        case SceneMode.SCENE3D:
            camera.frustum = transitioner._camera3D.frustum.clone();
            break;
        case SceneMode.COLUMBUS_VIEW:
            camera.frustum = transitioner._cameraCV.frustum.clone();
            break;
        case SceneMode.SCENE2D:
            camera.frustum = transitioner._camera2D.frustum.clone();
            break;
        }
    }

    function complete3DCallback(transitioner) {
        var scene = transitioner._scene;
        scene.mode = SceneMode.SCENE3D;
        setMorphTime(scene, 1.0);

        destroyMorphHandler(transitioner);

        updateFrustums(transitioner);
        var camera = scene.getCamera();
        camera.transform = Matrix4.IDENTITY.clone();

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

            // TODO: Match incoming columbus-view or 2D position
            camera.position = transitioner._camera3D.position.clone();
            camera.direction = transitioner._camera3D.direction.clone();
            camera.up = transitioner._camera3D.up.clone();
        }

        var wasMorphing = typeof transitioner._completeMorph !== 'undefined';
        transitioner._completeMorph = undefined;
        transitioner.onTransitionComplete.raiseEvent(transitioner, transitioner._previousMode, SceneMode.SCENE3D, wasMorphing);
    }

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

        scene.mode = SceneMode.SCENE2D;
        setMorphTime(scene, 0.0);

        destroyMorphHandler(transitioner);

        updateFrustums(transitioner);
        var camera = scene.getCamera();
        camera.transform = transitioner._camera2D.transform.clone();

        // TODO: Match incoming columbus-view or 3D position
        camera.position = transitioner._camera2D.position.clone();
        camera.direction = transitioner._camera2D.direction.clone();
        camera.up = transitioner._camera2D.up.clone();

        var wasMorphing = typeof transitioner._completeMorph !== 'undefined';
        transitioner._completeMorph = undefined;
        transitioner.onTransitionComplete.raiseEvent(transitioner, transitioner._previousMode, SceneMode.SCENE2D, wasMorphing);
    }

    function completeColumbusViewCallback(transitioner) {
        var scene = transitioner._scene;
        scene.mode = SceneMode.COLUMBUS_VIEW;
        setMorphTime(scene, 0.0);

        destroyMorphHandler(transitioner);

        updateFrustums(transitioner);
        var camera = scene.getCamera();
        camera.transform = transitioner._cameraCV.transform.clone();

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

            // TODO: Match incoming 2D or 3D position
            camera.position = transitioner._cameraCV.position.clone();
            camera.direction = transitioner._cameraCV.direction.clone();
            camera.up = transitioner._cameraCV.up.clone();
            camera.right = camera.direction.cross(camera.up);
        }

        var wasMorphing = typeof transitioner._completeMorph !== 'undefined';
        transitioner._completeMorph = undefined;
        transitioner.onTransitionComplete.raiseEvent(transitioner, transitioner._previousMode, SceneMode.COLUMBUS_VIEW, wasMorphing);
    }

    return SceneTransitioner;
});