/*global define*/
define([
        '../Core/DeveloperError',
        '../Core/Math',
        '../Core/Ellipsoid',
        '../Core/Cartesian3',
        '../Core/Cartesian4',
        '../Core/Matrix4',
        './CameraController',
        './PerspectiveFrustum'
    ], function(
        DeveloperError,
        CesiumMath,
        Ellipsoid,
        Cartesian3,
        Cartesian4,
        Matrix4,
        CameraController,
        PerspectiveFrustum) {
    "use strict";

    /**
     * The camera is defined by a position, orientation, and view frustum.
     * <br /><br />
     * The orientation forms an orthonormal basis with a view, up and right = view x up unit vectors.
     * <br /><br />
     * The viewing frustum is defined by 6 planes.
     * Each plane is represented by a {Cartesian4} object, where the x, y, and z components
     * define the unit vector normal to the plane, and the w component is the distance of the
     * plane from the origin/camera position.
     *
     * @alias Camera
     *
     * @exception {DeveloperError} canvas is required.
     *
     * @constructor
     *
     * @example
     * // Create a camera looking down the negative z-axis, positioned at the origin,
     * // with a field of view of 60 degrees, and 1:1 aspect ratio.
     * var camera = new Camera(canvas);
     * camera.position = new Cartesian3();
     * camera.direction = Cartesian3.UNIT_Z.negate();
     * camera.up = Cartesian3.UNIT_Y;
     * camera.frustum.fovy = CesiumMath.PI_OVER_THREE;
     * camera.frustum.near = 1.0;
     * camera.frustum.far = 2.0;
     *
     * @demo <a href="http://cesium.agi.com/Cesium/Apps/Sandcastle/index.html?src=Camera.html">Cesium Sandcastle Camera Demo</a>
     * @demo <a href="http://cesium.agi.com/Cesium/Apps/Sandcastle/index.html?src=Camera.html">Sandcastle Example</a> from the <a href="http://cesium.agi.com/2013/02/13/Cesium-Camera-Tutorial/">Camera Tutorial</a>
     */
    var Camera = function(canvas) {
        if (typeof canvas === 'undefined') {
            throw new DeveloperError('canvas is required.');
        }

        /**
         * Modifies the camera's reference frame. The inverse of this transformation is appended to the view matrix.
         *
         * @type {Matrix4}
         * @default {@link Matrix4.IDENTITY}
         *
         * @see Transforms
         */
        this.transform = Matrix4.IDENTITY.clone();
        this._transform = this.transform.clone();
        this._invTransform = Matrix4.IDENTITY.clone();

        var maxRadii = Ellipsoid.WGS84.getMaximumRadius();
        var position = new Cartesian3(0.0, -2.0, 1.0).normalize().multiplyByScalar(2.5 * maxRadii);

        /**
         * The position of the camera.
         *
         * @type {Cartesian3}
         */
        this.position = position.clone();
        this._position = position;
        this._positionWC = position;

        var direction = Cartesian3.ZERO.subtract(position).normalize();

        /**
         * The view direction of the camera.
         *
         * @type {Cartesian3}
         */
        this.direction = direction.clone();
        this._direction = direction;
        this._directionWC = direction;

        var right = direction.cross(Cartesian3.UNIT_Z).normalize();
        var up = right.cross(direction);

        /**
         * The up direction of the camera.
         *
         * @type {Cartesian3}
         */
        this.up = up.clone();
        this._up = up;
        this._upWC = up;

        right = direction.cross(up);

        /**
         * The right direction of the camera.
         *
         * @type {Cartesian3}
         */
        this.right = right.clone();
        this._right = right;
        this._rightWC = right;

        /**
         * The region of space in view.
         *
         * @type {Frustum}
         * @default PerspectiveFrustum()
         *
         * @see PerspectiveFrustum
         * @see PerspectiveOffCenterFrustum
         * @see OrthographicFrustum
         */
        this.frustum = new PerspectiveFrustum();
        this.frustum.fovy = CesiumMath.toRadians(60.0);
        this.frustum.aspectRatio = canvas.clientWidth / canvas.clientHeight;

        /**
         * Defines camera behavior. The controller can be used to perform common camera manipulations.
         *
         * @type {CameraController}
         * @default CameraController(this)
         */
        this.controller = new CameraController(this);

        this._viewMatrix = undefined;
        this._invViewMatrix = undefined;
        updateViewMatrix(this);

        this._canvas = canvas;
    };

    function updateViewMatrix(camera) {
        var r = camera._right;
        var u = camera._up;
        var d = camera._direction;
        var e = camera._position;

        var viewMatrix = new Matrix4( r.x,  r.y,  r.z, -r.dot(e),
                                      u.x,  u.y,  u.z, -u.dot(e),
                                     -d.x, -d.y, -d.z,  d.dot(e),
                                      0.0,  0.0,  0.0,      1.0);
        camera._viewMatrix = viewMatrix.multiply(camera._invTransform);
        camera._invViewMatrix = camera._viewMatrix.inverseTransformation();
    }

    function update(camera) {
        var position = camera._position;
        var positionChanged = !position.equals(camera.position);
        if (positionChanged) {
            position = camera._position = camera.position.clone();
        }

        var direction = camera._direction;
        var directionChanged = !direction.equals(camera.direction);
        if (directionChanged) {
            direction = camera._direction = camera.direction.clone();
        }

        var up = camera._up;
        var upChanged = !up.equals(camera.up);
        if (upChanged) {
            up = camera._up = camera.up.clone();
        }

        var right = camera._right;
        var rightChanged = !right.equals(camera.right);
        if (rightChanged) {
            right = camera._right = camera.right.clone();
        }

        var transform = camera._transform;
        var transformChanged = !transform.equals(camera.transform);
        if (transformChanged) {
            transform = camera._transform = camera.transform.clone();

            camera._invTransform = camera._transform.inverseTransformation();
        }

        if (positionChanged || transformChanged) {
            camera._positionWC = Cartesian3.fromCartesian4(transform.multiplyByPoint(position));
        }

        if (directionChanged || upChanged || rightChanged) {
            var det = direction.dot(up.cross(right));
            if (Math.abs(1.0 - det) > CesiumMath.EPSILON2) {
                //orthonormalize axes
                direction = camera._direction = direction.normalize();
                camera.direction = direction.clone();

                var invUpMag = 1.0 / up.magnitudeSquared();
                var scalar = up.dot(direction) * invUpMag;
                var w0 = direction.multiplyByScalar(scalar);
                up = camera._up = up.subtract(w0).normalize();
                camera.up = up.clone();

                right = camera._right = direction.cross(up);
                camera.right = right.clone();
            }
        }

        if (directionChanged || transformChanged) {
            camera._directionWC = Cartesian3.fromCartesian4(transform.multiplyByVector(new Cartesian4(direction.x, direction.y, direction.z, 0.0)));
        }

        if (upChanged || transformChanged) {
            camera._upWC = Cartesian3.fromCartesian4(transform.multiplyByVector(new Cartesian4(up.x, up.y, up.z, 0.0)));
        }

        if (rightChanged || transformChanged) {
            camera._rightWC = Cartesian3.fromCartesian4(transform.multiplyByVector(new Cartesian4(right.x, right.y, right.z, 0.0)));
        }

        if (positionChanged || directionChanged || upChanged || rightChanged || transformChanged) {
            updateViewMatrix(camera);
        }
    }

    /**
     * DOC_TBA
     *
     * @memberof Camera
     *
     * @return {Matrix4} DOC_TBA
     */
    Camera.prototype.getInverseTransform = function() {
        update(this);
        return this._invTransform;
    };

    /**
     * Returns the view matrix.
     *
     * @memberof Camera
     *
     * @return {Matrix4} The view matrix.
     *
     * @see UniformState#getView
     * @see UniformState#setView
     * @see czm_view
     */
    Camera.prototype.getViewMatrix = function() {
        update(this);
        return this._viewMatrix;
    };

    /**
     * DOC_TBA
     * @memberof Camera
     */
    Camera.prototype.getInverseViewMatrix = function() {
        update(this);
        return this._invViewMatrix;
    };

    /**
     * The position of the camera in world coordinates.
     *
     * @type {Cartesian3}
     */
    Camera.prototype.getPositionWC = function() {
        update(this);
        return this._positionWC;
    };

    /**
     * The view direction of the camera in world coordinates.
     *
     * @type {Cartesian3}
     */
    Camera.prototype.getDirectionWC = function() {
        update(this);
        return this._directionWC;
    };

    /**
     * The up direction of the camera in world coordinates.
     *
     * @type {Cartesian3}
     */
    Camera.prototype.getUpWC = function() {
        update(this);
        return this._upWC;
    };

    /**
     * The right direction of the camera in world coordinates.
     *
     * @type {Cartesian3}
     */
    Camera.prototype.getRightWC = function() {
        update(this);
        return this._rightWC;
    };

    /**
     * Returns a duplicate of a Camera instance.
     *
     * @memberof Camera
     *
     * @return {Camera} A new copy of the Camera instance.
     */
    Camera.prototype.clone = function() {
        var camera = new Camera(this._canvas);
        camera.position = this.position.clone();
        camera.direction = this.direction.clone();
        camera.up = this.up.clone();
        camera.right = this.right.clone();
        camera.transform = this.transform.clone();
        camera.frustum = this.frustum.clone();
        return camera;
    };

    /**
     * Transform a vector or point from world coordinates to the camera's reference frame.
     * @memberof Camera
     *
     * @param {Cartesian4} cartesian The vector or point to transform.
     * @param {Cartesian4} [result] The object onto which to store the result.
     *
     * @exception {DeveloperError} cartesian is required.
     *
     * @returns {Cartesian4} The transformed vector or point.
     */
    Camera.prototype.worldToCameraCoordinates = function(cartesian, result) {
        if (typeof cartesian === 'undefined') {
            throw new DeveloperError('cartesian is required.');
        }
        return Matrix4.multiplyByVector(this.getInverseTransform(), cartesian, result);
    };

    /**
     * Transform a vector or point from the camera's reference frame to world coordinates.
     * @memberof Camera
     *
     * @param {Cartesian4} vector The vector or point to transform.
     * @param {Cartesian4} [result] The object onto which to store the result.
     *
     * @exception {DeveloperError} cartesian is required.
     *
     * @returns {Cartesian4} The transformed vector or point.
     */
    Camera.prototype.cameraToWorldCoordinates = function(cartesian, result) {
        if (typeof cartesian === 'undefined') {
            throw new DeveloperError('cartesian is required.');
        }
        return Matrix4.multiplyByVector(this.transform, cartesian, result);
    };

    return Camera;
});