"use strict";

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Represents perspective first person camera, whose rotation is specified by 2 angles (horizontal and vertical).
 * Connected with underlying Three.js camera which is actually used for rendering.
 *
 * @author smrceant (Antonin Smrcek)
 *
 * @constructor
 */
var Camera = function ()
{
  // TODO watch out for that window.innerWidth/height

  /**
   * Underlying Three.js camera object used for rendering.
   * @type {THREE.PerspectiveCamera}
   */
  this.cameraThree = new THREE.PerspectiveCamera (Preferences.Camera.FOV_DEFAULT_DEGREES, appMain.getRenderingWindow().windowRatio, Preferences.Camera.Z_NEAR_DEFAULT, Preferences.Camera.Z_FAR_DEFAULT );
  this.cameraThree.matrixAutoUpdate = false;

  /**
   * Which entity (e.g. player) owns this camera.
   *
   * @type {Entity}
   */
  this.ownerEntity = null;

  /**
   * What was the last time when the mouse was moving.
   *
   * @type {number}
   */
  this.lastViewChangeTime = Date.now();

  /**
   *
   * @type {THREE.Vector3}
   */
  this.directionVector = new THREE.Vector3();

  /**
   *
   * @type {THREE.Vector3}
   */
  this.rightVector = new THREE.Vector3();

  /**
   * Vector with real amount of move in the forward direction.
   *
   * @type {THREE.Vector3}
   */
  this.scaledDirectionVector = new THREE.Vector3();

  /**
   * Vector with real amount of move in the side direction.
   *
   * @type {THREE.Vector3}
   */
  this.scaledRightVector = new THREE.Vector3();

  /**
   * Characterizes movement of entity. Probably same as scaledDirectionVector!
   *
   * @type {THREE.Vector3}
   */
  this.moveVector = new THREE.Vector3();

  /**
   * Length of moveVector. For collision computations.
   *
   * @type {number}
   */
  this.moveVectorLength = 0;

  /**
   *
   * @type {THREE.Vector3}
   * @private
   */
  this._tmp_scaled_rightOrDirectionVector = new THREE.Vector3();

  /**
   *
   * @type {THREE.Vector3}
   */
  this.upVector = new THREE.Vector3 (0, 1, 0); // standard up vector

  /**
   * Position of the camera in the world.
   *
   * @type {THREE.Vector3}
   */
  this.position = new THREE.Vector3();

  this.angleHorizontal = 0.0; // also known as phi, range [0,360) degrees; but stored as radians!
  this.angleVertical = 0.0; // also known as theta, range [0,180) degrees; but stored as radians!

  /**
   * Speed of camera movement.
   *
   * @type {number}
   */
  this.speed = Preferences.Camera.CAMERA_MOVE_SPEED_DEFAULT;

  /**
   * Speed of camera rotation.
   *
   * @type {number}
   */
  this.angleChangeAmount = Preferences.Camera.CAMERA_ANGLE_CHANGE_AMOUNT_DEFAULT;

  /**
   * Position of the camera before it was moved, e.g. during the collision test.
   *
   * @type {THREE.Vector3}
   */
  this.oldPosition = new THREE.Vector3();

  /**
   * We actually render the view from Y+1 or something like that, simulating avatar height.
   *
   * @type {THREE.Vector3}
   */
  this.positionWithOffset = new THREE.Vector3();

  /**
   * Position of Three.js camera
   *
   * @type {THREE.Vector3}
   */
  this.cameraCenter = new THREE.Vector3();

  /**
   * Indicates that camera has changed its position in current game loop and should be updated.
   *
   * @type {boolean}
   */
  this.viewChanged = false;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// CONSTANTS (CAMERA MOVEMENT)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

Camera.MOVE_FORWARD       = 0;
Camera.MOVE_BACK          = 1;
Camera.MOVE_FORWARD_WHEEL = 2;
Camera.MOVE_BACK_WHEEL    = 3;
Camera.MOVE_LEFT          = 4;
Camera.MOVE_RIGHT         = 5;
Camera.MOVE_UP            = 6;
Camera.MOVE_DOWN          = 7;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// CAMERA-ENTITY OWNERSHIP
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Sets an entity which uses this camera for displaying the world (e.g. player).
 *
 * @param {Entity}  entity    owner of this camera
 */
Camera.prototype.setOwnerEntity = function (entity)
{
  this.ownerEntity = entity;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// VIEWPOINT SWITCHING AND INFO
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Configures camera to the given viewpoint.
 *
 * @param {Viewpoint} viewpoint   contains info about new camera position and rotation (orientation)
 */
Camera.prototype.switchViewpoint = function (viewpoint)
{
    this.position = viewpoint.getPosition();
    this.angleHorizontal = viewpoint.getAngleHorizontal();
    this.angleVertical = viewpoint.getAngleVertical();

    this.ownerEntity.updateLastHeightmapHeight();

    this._viewChange();
};


/**
 * [DEV] Utility function for retrieving viewpoint definition of current camera position.
 *
 * @returns {string}    current viewpoint definition JavaScript code
 */
Camera.prototype.getViewpointDefinition = function()
{
  return "Preferences.Viewpoint.VIEWPOINT_NAME = new Viewpoint (new THREE.Vector3("+this.position.x+", "+this.position.y+", "+this.position.z+"), "+this.angleHorizontal+", "+this.angleVertical+", \"\");";
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MOVING CAMERA AROUND, CHANGING CAMERA PROPERTIES
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

Camera.prototype.rotateHorizontal = function(direction, speed)
{
    this.angleHorizontal = Camera.normalizeAngle (this.angleHorizontal + (direction == Camera.MOVE_LEFT ? 1 : -1) * speed);
}

Camera.prototype.rotateVertical = function(direction, speed)
{
    // no need to normalize, since we restrict the range... it would even be a bother to normalize, since we need to be in range (-90 deg, 90 deg)
    var newAngle = this.angleVertical + (direction == Camera.MOVE_FORWARD ? 1 : -1) * speed;

    if (newAngle > Preferences.Camera.VERTICAL_ANGLE_MIN_LIMIT && newAngle < Preferences.Camera.VERTICAL_ANGLE_MAX_LIMIT) {
        this.angleVertical = newAngle;
    }

    return this.angleVertical != newAngle; // return true if angle changed
}

/**
 * Moves the camera angles in the desired direction (position remains same!). Possible directions are Camera.MOVE_LEFT,
 * Camera.MOVE_RIGHT, Camera.MOVE_FORWARD, Camera.MOVE_BACK.
 *
 * @param {number}  direction   in which direction will camera angles move
 */
Camera.prototype.moveAngle = function (direction)
{
    var speed = this.angleChangeAmount * Preferences.Control.MOUSE_SENSITIVITY;

    if(direction === Camera.MOVE_LEFT || direction === Camera.MOVE_RIGHT) {
        this.rotateHorizontal(direction, speed);
    }
    else if((direction === Camera.MOVE_FORWARD || direction === Camera.MOVE_BACK) && !this.rotateVertical(direction, speed)) {
        return; // skip rest if no change
    }

    this._flagViewChanged();
};

/**
 * Moves the camera position in desired direction. Possible directions are Camera.MOVE_LEFT, Camera.MOVE_RIGHT,
 * Camera.MOVE_FORWARD, Camera.MOVE_BACK.
 *
 * @param {number} direction        in which direction will be camera position moved
 * @param {number} moveParamValue   number between [1,0) which will be used to further alter the amount of the move
 */
Camera.prototype.moveCamera = function (direction, moveParamValue)
{
  // if not flying, do not allow move in Y direction
  if (this.ownerEntity.inWalkMode() && Preferences.Collision.__COLLISION_DETECTION)
  {
    this.directionVector.y = 0.0;
    this.rightVector.y     = 0.0;
  }

  var speed = this.speed * Preferences.Control.MOVING_SPEED * appMain.getLastFrameDeltaRatio() * moveParamValue;
  var wheelSpeed = Preferences.Control.MOUSE_WHEEL_SENSITIVITY_BASE;

  var actions = [
      {direction: Camera.MOVE_LEFT, vector: this.rightVector, sign: -1, wheel: 1, scaleVector: this.scaledRightVector},
      {direction: Camera.MOVE_RIGHT, vector: this.rightVector, sign: 1, wheel: 1, scaleVector: this.scaledRightVector},
      {direction: Camera.MOVE_FORWARD, vector: this.directionVector, sign: 1, wheel: 1, scaleVector: this.scaledDirectionVector},
      {direction: Camera.MOVE_BACK, vector: this.directionVector, sign: -1, wheel: 1, scaleVector: this.scaledDirectionVector},
      {direction: Camera.MOVE_FORWARD_WHEEL, vector: this.directionVector, sign: 1, wheel: wheelSpeed, scaleVector: this.scaledDirectionVector},
      {direction: Camera.MOVE_BACK_WHEEL, vector: this.directionVector, sign: -1, wheel: wheelSpeed, scaleVector: this.scaledDirectionVector},
      {direction: Camera.MOVE_UP, vector: this.upVector, sign: 1, wheel: 1},
      {direction: Camera.MOVE_DOWN, vector: this.upVector, sign: -1, wheel: 1},
  ];

  // change in position of the camera depending on given direction
  for(var i = 0; i < actions.length; i ++) {
      if(actions[i].direction == direction) {
          this._tmp_scaled_rightOrDirectionVector.copy(actions[i].vector).multiplyScalar(speed * actions[i].sign * actions[i].wheel);

          if(actions[i].scaleVector) {
              actions[i].scaleVector.copy(this._tmp_scaled_rightOrDirectionVector);
          }
      }
  }

  // actually, the moveVector always equals the scaledDirectionVector
  this.moveVector.addVectors (this.scaledDirectionVector, this.scaledRightVector);
  this.moveVectorLength = this.moveVector.length();

  this.position.add (this._tmp_scaled_rightOrDirectionVector);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// UPDATING CAMERA, SAVING CURRENT POSITION
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Updates camera matrices if necessary. Called after all collision computations are done.
 */
Camera.prototype.updateCamera = function()
{
  if (this.viewChanged)
  {
    this._viewChange();
  }
};

/**
 * Saves current camera position into another variable, so it can be retrieved later if needed.
 */
Camera.prototype.saveCurrentPosition = function()
{
  this.oldPosition.copy (this.position);
};

/**
 * Restores previously saved (old) camera position.
 */
Camera.prototype.restoreOldPosition = function()
{
  this.position.copy (this.oldPosition);
};

Camera.prototype.getPosition = function () {
	return this.position;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// CHANGING CAMERA PROPERTIES (POSITION, HEIGHT, FOV, ...)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Assigns new position to the camera.
 *
 * @param {THREE.Vector3}     newPosition               new camera position
 * @param {boolean}           [updateViewImmediately]   TRUE if camera should call its update methods immediately
 */
Camera.prototype.setPosition = function (newPosition, updateViewImmediately)
{
  this.position.copy (newPosition);

  this._flagViewChanged();

  if (updateViewImmediately)
  {
    this._viewChange();
  }
};

/**
 * Changes Y position (altitude) of the camera by a desired amount.
 *
 * @param {number}  deltaY    relative change of the camera Y
 */
Camera.prototype.changeY = function (deltaY)
{
  this.position.y += deltaY/* + Preferences.PLAYER_HEAD_ABOVE_CAMERA_METERS*/;

  this._flagViewChanged();
};

/**
 * Sets new camera Y position.
 *
 * @param {number} y    new camera Y position
 */
Camera.prototype.setY = function (y)
{
  // include offset which simulates height of the player
  this.position.y = y/* + Preferences.PLAYER_HEAD_ABOVE_CAMERA_METERS*/;

  this._flagViewChanged();
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// CALLBACKS CALLED WHEN VIEW OR PROJECTION, OR BOTH CHANGE
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
* Computes Projection-View matrix. Must be called every time when projection or view matrix changes!
*
* @private
*/
Camera.prototype._projectionViewChange = function()
{
  this.cameraThree.updateMatrix();
};

/**
 * Called when view parameters (camera position or angles) change. Time of the last view change is recorded, picking
 * map is invalidated and flag indicating that view was changed is restored to false.
 *
 * @private
 */
Camera.prototype._viewChange = function()
{
  // IMPORTANT - we set flag to false again for the next main loop!
  this.viewChanged = false;

  this.lastViewChangeTime = Date.now();

  // since we will change view now, we won't have picking map for it
  picking.invalidatePickingMap();

  this._computeViewMatrix();
  this._projectionViewChange();
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// COMPUTING VIEW MATRIX AND NECESSARY VECTORS, COMPUTING PROJECTION MATRIX
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Computes direction vector of the camera (vector from EYE to AT). See http://www.ntu.edu.sg/home/ehchua/programming/opengl/images/Graphics3D_CameraSpace.png
 *
 * @private
 */
Camera.prototype._computeDirectionVector = function()
{
  // transform sphere coordinates to Euclidean coordinates
    // based on http://www.opengl-tutorial.org/beginners-tutorials/tutorial-6-keyboard-and-mouse/
  this.directionVector.x = Math.sin (this.angleHorizontal) * Math.cos (this.angleVertical);
  this.directionVector.y = Math.sin (this.angleVertical);
  this.directionVector.z = Math.cos (this.angleHorizontal) * Math.cos (this.angleVertical);
};

/**
 * Computes right vector of the camera. See http://www.ntu.edu.sg/home/ehchua/programming/opengl/images/Graphics3D_CameraSpace.png
 *
 * @private
 */
Camera.prototype._computeRightVector = function()
{
  //this._tmp_angleDif = this.angleHorizontal - Preferences.Math.PI_HALF;

  /*this.rightVector.x = Math.sin (this._tmp_angleDif);
  this.rightVector.y = 0; // always 0
  this.rightVector.z = Math.cos (this._tmp_angleDif);*/

  this.rightVector.crossVectors (this.directionVector, this.upVector);
};

/**
 *
 * @private
 */
Camera.prototype._computeUpVector = function()
{
  // up vector is perpendicular to the right and direction vector, so we are able to obtain it by cross product of these two
  this.upVector.crossVectors (this.rightVector, this.directionVector);
};

/**
 * Computes view matrix of the camera.
 *
 * @private
 */
Camera.prototype._computeViewMatrix = function()
{
  // transform spherical coordinates to cartesian and obtain vector from camera position to the point where we look (direction vector)

  this._computeDirectionVector();
  // now we need to compute two of the three vectors (right, direction and up) and the last one can be obtained as a cross product of those two

  // our solution: are computing both right and direction vector using trigonometry magic;
  // we could set "up vector" to standard (0,1,0) and do trigonometry magic just for example on direction vector and then compute right vector as cross product of those two,
  // but unfortunately these kind of solutions terribly fail when our viewing angle is (even close to being) perpendicular to the ground; this works ok
  this._computeRightVector();

  // we have to maintain real camera Y and add offset for player head each time here
   // cannot ever mix up with camera.position! That would mess up our gravity calculations etc.
  //vec3.copy (this.positionWithOffset, this.position);
  this.positionWithOffset.copy (this.position);
  this.positionWithOffset.y += Preferences.Camera.PLAYER_HEAD_ABOVE_CAMERA_METERS;

  // compute coordinates of the point where camera looks (camera center)
  this.cameraCenter.addVectors (this.positionWithOffset, this.directionVector); // positionWithOffset = EYE

  this.cameraThree.position.copy (this.positionWithOffset);

  this.cameraThree.lookAt (this.cameraCenter);

  // IMPORTANT NOTICE ONCE AGAIN:
  ////////////////////////////////////////////////////////////////////////////
  // !!!!!!!!!! VIEW MATRIX IS COMPUTED FROM positionWithOffset, NOT FROM position !!!!!!!!!!
  ////////////////////////////////////////////////////////////////////////////
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Sets to true the flag which indicates that view has changed.
 */
Camera.prototype._flagViewChanged = function()
{
  this.viewChanged = true;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// AUXILIARY METHODS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Normalizes angle to the [0,2PI) range.
 *
 * @param   {number}  angleInRadians    value of the angle in radians to be normalized
 * @returns {number}                    angle in radians between 0 and 2PI
 */
Camera.normalizeAngle = function (angleInRadians)
{
    return (angleInRadians + Preferences.Math.PI_2) % Preferences.Math.PI_2;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// GETTERS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Retrieves underlying Three.js camera.
 *
 * @returns {THREE.PerspectiveCamera}
 */
Camera.prototype.getCameraThree = function()
{
    return this.cameraThree;
};
