text: // Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd.
  
  import { Vec3, Mat4, lerp, Vec4 } from '../../core/value-types';
  import { Ray } from '../../core/geom-utils';
  import enums from '../enums';
  
  let _tmp_mat4 = new Mat4();
  
  let _matView = new Mat4();
  let _matViewInv = new Mat4();
  let _matProj = new Mat4();
  let _matViewProj = new Mat4();
  let _matInvViewProj = new Mat4();
  let _tmp_v3 = new Vec3();
  let _tmp2_v3 = new Vec3();
  
  /**
   * A representation of a camera instance
   */
  export default class Camera {
    _poolID = -1;
    _node = null;
    _projection = enums.PROJ_PERSPECTIVE;
  
    // priority. the smaller one will be rendered first
    _priority = 0;
  
    // clear options
    _color = new Vec4(0.2, 0.3, 0.47, 1);
    _depth = 1;
    _stencil = 0;
    _clearFlags = enums.CLEAR_COLOR | enums.CLEAR_DEPTH;
    _clearModel = null;
  
    // stages & framebuffer
    _stages = [];
    _framebuffer = null;
  
    // projection properties
    _near = 0.01;
    _far = 1000.0;
    _fov = Math.PI / 4.0; // vertical fov
    _rect = {
      x: 0, y: 0, w: 1, h: 1
    };
  
    // ortho properties
    _orthoHeight = 10;
  
    _cullingMask = 0xffffffff;
  
  
    // culling mask
    get cullingMask () {
      return this._cullingMask;
    }
  
    set cullingMask (mask) {
      this._cullingMask = mask;
    }
  
    setCullingMask (mask) {
      this._cullingMask = mask;
    }
  
    /**
     * Get the hosting node of this camera
     * @returns {Node} the hosting node
     */
    getNode () {
      return this._node;
    }
  
    /**
     * Set the hosting node of this camera
     * @param {Node} node the hosting node
     */
    setNode (node) {
      this._node = node;
    }
  
    /**
     * Get the projection type of the camera
     * @returns {number} camera projection type
     */
    getType () {
      return this._projection;
    }
  
    /**
     * Set the projection type of the camera
     * @param {number} type camera projection type
     */
    setType (type) {
      this._projection = type;
    }
  
    /**
     * Get the priority of the camera
     * @returns {number} camera priority
     */
    getPriority () {
      return this._priority;
    }
  
    /**
     * Set the priority of the camera
     * @param {number} priority camera priority
     */
    setPriority (priority) {
      this._priority = priority;
    }
  
    /**
     * Get the orthogonal height of the camera
     * @returns {number} camera height
     */
    getOrthoHeight () {
      return this._orthoHeight;
    }
  
    /**
     * Set the orthogonal height of the camera
     * @param {number} val camera height
     */
    setOrthoHeight (val) {
      this._orthoHeight = val;
    }
  
    /**
     * Get the field of view of the camera
     * @returns {number} camera field of view
     */
    getFov () {
      return this._fov;
    }
  
    /**
     * Set the field of view of the camera
     * @param {number} fov camera field of view
     */
    setFov (fov) {
      this._fov = fov;
    }
  
    /**
     * Get the near clipping distance of the camera
     * @returns {number} camera near clipping distance
     */
    getNear () {
      return this._near;
    }
  
    /**
     * Set the near clipping distance of the camera
     * @param {number} near camera near clipping distance
     */
    setNear (near) {
      this._near = near;
    }
  
    /**
     * Get the far clipping distance of the camera
     * @returns {number} camera far clipping distance
     */
    getFar () {
      return this._far;
    }
  
    /**
     * Set the far clipping distance of the camera
     * @param {number} far camera far clipping distance
     */
    setFar (far) {
      this._far = far;
    }
  
    /**
     * Get the clear color of the camera
     * @returns {Vec4} out the receiving color vector
     */
    getColor (out) {
      return Vec4.copy(out, this._color);
    }
  
    /**
     * Set the clear color of the camera
     * @param {number} r red channel of camera clear color
     * @param {number} g green channel of camera clear color
     * @param {number} b blue channel of camera clear color
     * @param {number} a alpha channel of camera clear color
     */
    setColor (r, g, b, a) {
      Vec4.set(this._color, r, g, b, a);
    }
  
    /**
     * Get the clear depth of the camera
     * @returns {number} camera clear depth
     */
    getDepth () {
      return this._depth;
    }
  
    /**
     * Set the clear depth of the camera
     * @param {number} depth camera clear depth
     */
    setDepth (depth) {
      this._depth = depth;
    }
  
    /**
     * Get the clearing stencil value of the camera
     * @returns {number} camera clearing stencil value
     */
    getStencil () {
      return this._stencil;
    }
  
    /**
     * Set the clearing stencil value of the camera
     * @param {number} stencil camera clearing stencil value
     */
    setStencil (stencil) {
      this._stencil = stencil;
    }
  
    /**
     * Get the clearing flags of the camera
     * @returns {number} camera clearing flags
     */
    getClearFlags () {
      return this._clearFlags;
    }
  
    /**
     * Set the clearing flags of the camera
     * @param {number} flags camera clearing flags
     */
    setClearFlags (flags) {
      this._clearFlags = flags;
    }
  
    /**
     * Get the rect of the camera
     * @param {Object} out the receiving object
     * @returns {Object} camera rect
     */
    getRect (out) {
      out.x = this._rect.x;
      out.y = this._rect.y;
      out.w = this._rect.w;
      out.h = this._rect.h;
  
      return out;
    }
  
    /**
     * Set the rect of the camera
     * @param {Number} x - [0,1]
     * @param {Number} y - [0,1]
     * @param {Number} w - [0,1]
     * @param {Number} h - [0,1]
     */
    setRect (x, y, w, h) {
      this._rect.x = x;
      this._rect.y = y;
      this._rect.w = w;
      this._rect.h = h;
    }
  
    /**
     * Get the stages of the camera
     * @returns {string[]} camera stages
     */
    getStages () {
      return this._stages;
    }
  
    /**
     * Set the stages of the camera
     * @param {string[]} stages camera stages
     */
    setStages (stages) {
      this._stages = stages;
    }
  
    /**
     * Get the framebuffer of the camera
     * @returns {FrameBuffer} camera framebuffer
     */
    getFramebuffer () {
      return this._framebuffer;
    }
  
    /**
     * Set the framebuffer of the camera
     * @param {FrameBuffer} framebuffer camera framebuffer
     */
    setFrameBuffer (framebuffer) {
      this._framebuffer = framebuffer;
    }
  
    _calcMatrices (width, height) {
      // view matrix
      this._node.getWorldRT(_matViewInv);
      Mat4.invert(_matView, _matViewInv);
  
      // projection matrix
      let aspect = width / height;
      if (this._projection === enums.PROJ_PERSPECTIVE) {
        Mat4.perspective(_matProj,
          this._fov,
          aspect,
          this._near,
          this._far
        );
      } else {
        let x = this._orthoHeight * aspect;
        let y = this._orthoHeight;
        Mat4.ortho(_matProj,
          -x, x, -y, y, this._near, this._far
        );
      }
  
      // view-projection
      Mat4.mul(_matViewProj, _matProj, _matView);
      // inv view-projection
      Mat4.invert(_matInvViewProj, _matViewProj);
    }
  
    /**
     * extract a view of this camera
     * @param {View} out the receiving view
     * @param {number} width framebuffer width
     * @param {number} height framebuffer height
     */
    extractView (out, width, height) {
      if (this._framebuffer) {
        width = this._framebuffer._width;
        height = this._framebuffer._height;
      }
  
      // priority
      out._priority = this._priority;
  
      // rect
      out._rect.x = this._rect.x * width;
      out._rect.y = this._rect.y * height;
      out._rect.w = this._rect.w * width;
      out._rect.h = this._rect.h * height;
  
      // clear opts
      this.getColor(out._color);
      out._depth = this._depth;
      out._stencil = this._stencil;
      out._clearFlags = this._clearFlags;
      out._clearModel = this._clearModel;
  
      // stages & framebuffer
      out._stages = this._stages;
      out._framebuffer = this._framebuffer;
  
      this._calcMatrices(width, height);
      Mat4.copy(out._matView, _matView);
      Mat4.copy(out._matViewInv, _matViewInv);
      Mat4.copy(out._matProj, _matProj);
      Mat4.copy(out._matViewProj, _matViewProj);
      Mat4.copy(out._matInvViewProj, _matInvViewProj);
  
      out._cullingMask = this._cullingMask;
    }
  
    /**
     * transform a screen position to a world space ray
     * @param {number} x the screen x position to be transformed
     * @param {number} y the screen y position to be transformed
     * @param {number} width framebuffer width
     * @param {number} height framebuffer height
     * @param {Ray} out the resulting ray
     * @returns {Ray} the resulting ray
     */
    screenPointToRay (x, y, width, height, out) {
      if (!cc.geomUtils) return out;
  
      out = out || new Ray();
      this._calcMatrices(width, height);
  
      let cx = this._rect.x * width;
      let cy = this._rect.y * height;
      let cw = this._rect.w * width;
      let ch = this._rect.h * height;
  
      // far plane intersection
      Vec3.set(_tmp2_v3, (x - cx) / cw * 2 - 1, (y - cy) / ch * 2 - 1, 1);
      Vec3.transformMat4(_tmp2_v3, _tmp2_v3, _matInvViewProj);
  
      if (this._projection === enums.PROJ_PERSPECTIVE) {
        // camera origin
        this._node.getWorldPosition(_tmp_v3);
      } else {
        // near plane intersection
        Vec3.set(_tmp_v3, (x - cx) / cw * 2 - 1, (y - cy) / ch * 2 - 1, -1);
        Vec3.transformMat4(_tmp_v3, _tmp_v3, _matInvViewProj);
      }
  
      return Ray.fromPoints(out, _tmp_v3, _tmp2_v3);
    }
  
    /**
     * transform a screen position to world space
     * @param {Vec3} out the resulting vector
     * @param {Vec3} screenPos the screen position to be transformed
     * @param {number} width framebuffer width
     * @param {number} height framebuffer height
     * @returns {Vec3} the resulting vector
     */
    screenToWorld (out, screenPos, width, height) {
      this._calcMatrices(width, height);
  
      let cx = this._rect.x * width;
      let cy = this._rect.y * height;
      let cw = this._rect.w * width;
      let ch = this._rect.h * height;
  
      if (this._projection === enums.PROJ_PERSPECTIVE) {
        // calculate screen pos in far clip plane
        Vec3.set(out,
          (screenPos.x - cx) / cw * 2 - 1,
          (screenPos.y - cy) / ch * 2 - 1,
          0.9999
        );
  
        // transform to world
        Vec3.transformMat4(out, out, _matInvViewProj);
  
        // lerp to depth z
        this._node.getWorldPosition(_tmp_v3);
  
        Vec3.lerp(out, _tmp_v3, out, lerp(this._near / this._far, 1, screenPos.z));
      } else {
        Vec3.set(out,
          (screenPos.x - cx) / cw * 2 - 1,
          (screenPos.y - cy) / ch * 2 - 1,
          screenPos.z * 2 - 1
        );
  
        // transform to world
        Vec3.transformMat4(out, out, _matInvViewProj);
      }
  
      return out;
    }
  
    /**
     * transform a world space position to screen space
     * @param {Vec3} out the resulting vector
     * @param {Vec3} worldPos the world space position to be transformed
     * @param {number} width framebuffer width
     * @param {number} height framebuffer height
     * @returns {Vec3} the resulting vector
     */
    worldToScreen (out, worldPos, width, height) {
      this._calcMatrices(width, height);
  
      let cx = this._rect.x * width;
      let cy = this._rect.y * height;
      let cw = this._rect.w * width;
      let ch = this._rect.h * height;
  
      Vec3.transformMat4(out, worldPos, _matViewProj);
      out.x = cx + (out.x + 1) * 0.5 * cw;
      out.y = cy + (out.y + 1) * 0.5 * ch;
      out.z = out.z * 0.5 + 0.5;
  
      return out;
    }
  
    /**
     * transform a world space matrix to screen space
     * @param {Mat4} out the resulting vector
     * @param {Mat4} worldMatrix the world space matrix to be transformed
     * @param {number} width framebuffer width
     * @param {number} height framebuffer height
     * @returns {Mat4} the resulting vector
     */
    worldMatrixToScreen (out, worldMatrix, width, height) {
      this._calcMatrices(width, height);
  
      Mat4.mul(out, _matViewProj, worldMatrix);
  
      let halfWidth = width / 2;
      let halfHeight = height / 2;
      Mat4.identity(_tmp_mat4);
      Mat4.transform(_tmp_mat4, _tmp_mat4, Vec3.set(_tmp_v3, halfWidth, halfHeight, 0));
      Mat4.scale(_tmp_mat4, _tmp_mat4, Vec3.set(_tmp_v3, halfWidth, halfHeight, 1));
  
      Mat4.mul(out, _tmp_mat4, out);
  
      return out;
    }
  }
  