var Point3d = require('./Point3d');

/**
 * @class Camera
 * The camera is mounted on a (virtual) camera arm. The camera arm can rotate
 * The camera is always looking in the direction of the origin of the arm.
 * This way, the camera always rotates around one fixed point, the location
 * of the camera arm.
 *
 * Documentation:
 *   http://en.wikipedia.org/wiki/3D_projection
 */
function Camera() {
  this.armLocation = new Point3d();
  this.armRotation = {};
  this.armRotation.horizontal = 0;
  this.armRotation.vertical = 0;
  this.armLength = 1.7;
  this.cameraOffset = new Point3d();
  this.offsetMultiplier = 0.6;

  this.cameraLocation = new Point3d();
  this.cameraRotation =  new Point3d(0.5*Math.PI, 0, 0);

  this.calculateCameraOrientation();
}

/**
 * Set offset camera in camera coordinates
 * @param {Number} x offset by camera horisontal
 * @param {Number} y offset by camera vertical
 */
Camera.prototype.setOffset = function(x, y) {
  var abs = Math.abs,
      sign = Math.sign,
      mul = this.offsetMultiplier,
      border = this.armLength * mul;

  if (abs(x) > border) {
      x = sign(x) * border;
  }
  if (abs(y) > border) {
      y = sign(y) * border;
  }
  this.cameraOffset.x = x;
  this.cameraOffset.y = y;
  this.calculateCameraOrientation();
};

/**
 * Get camera offset by horizontal and vertical
 * @return {Point3d} x - horizontal offset, y - vertical offset, z - not used
 */
Camera.prototype.getOffset = function(x, y) {
  return this.cameraOffset;
};

/**
 * Set the location (origin) of the arm
 * @param {Number} x  Normalized value of x
 * @param {Number} y  Normalized value of y
 * @param {Number} z  Normalized value of z
 */
Camera.prototype.setArmLocation = function(x, y, z) {
  this.armLocation.x = x;
  this.armLocation.y = y;
  this.armLocation.z = z;

  this.calculateCameraOrientation();
};

/**
 * Set the rotation of the camera arm
 * @param {Number} horizontal   The horizontal rotation, between 0 and 2*PI.
 *                Optional, can be left undefined.
 * @param {Number} vertical   The vertical rotation, between 0 and 0.5*PI
 *                if vertical=0.5*PI, the graph is shown from the
 *                top. Optional, can be left undefined.
 */
Camera.prototype.setArmRotation = function(horizontal, vertical) {
  if (horizontal !== undefined) {
    this.armRotation.horizontal = horizontal;
  }

  if (vertical !== undefined) {
    this.armRotation.vertical = vertical;
    if (this.armRotation.vertical < 0) this.armRotation.vertical = 0;
    if (this.armRotation.vertical > 0.5*Math.PI) this.armRotation.vertical = 0.5*Math.PI;
  }

  if (horizontal !== undefined || vertical !== undefined) {
    this.calculateCameraOrientation();
  }
};

/**
 * Retrieve the current arm rotation
 * @return {object}   An object with parameters horizontal and vertical
 */
Camera.prototype.getArmRotation = function() {
  var rot = {};
  rot.horizontal = this.armRotation.horizontal;
  rot.vertical = this.armRotation.vertical;

  return rot;
};

/**
 * Set the (normalized) length of the camera arm.
 * @param {Number} length A length between 0.71 and 5.0
 */
Camera.prototype.setArmLength = function(length) {
  if (length === undefined)
    return;

  this.armLength = length;

  // Radius must be larger than the corner of the graph,
  // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the
  // graph
  if (this.armLength < 0.71) this.armLength = 0.71;
  if (this.armLength > 5.0) this.armLength = 5.0;

  this.setOffset(this.cameraOffset.x, this.cameraOffset.y);
  this.calculateCameraOrientation();
};

/**
 * Retrieve the arm length
 * @return {Number} length
 */
Camera.prototype.getArmLength = function() {
  return this.armLength;
};

/**
 * Retrieve the camera location
 * @return {Point3d} cameraLocation
 */
Camera.prototype.getCameraLocation = function() {
  return this.cameraLocation;
};

/**
 * Retrieve the camera rotation
 * @return {Point3d} cameraRotation
 */
Camera.prototype.getCameraRotation = function() {
  return this.cameraRotation;
};

/**
 * Calculate the location and rotation of the camera based on the
 * position and orientation of the camera arm
 */
Camera.prototype.calculateCameraOrientation = function() {
  // calculate location of the camera
  this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
  this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
  this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical);

  // calculate rotation of the camera
  this.cameraRotation.x = Math.PI/2 - this.armRotation.vertical;
  this.cameraRotation.y = 0;
  this.cameraRotation.z = -this.armRotation.horizontal;

  var xa = this.cameraRotation.x;
  var ya = this.cameraRotation.y;
  var za = this.cameraRotation.z;
  var dx = this.cameraOffset.x;
  var dy = this.cameraOffset.y;
  var sin = Math.sin, cos = Math.cos;

  this.cameraLocation.x = this.cameraLocation.x + dx * cos(za) + dy * - sin(za) * cos(xa);
  this.cameraLocation.y = this.cameraLocation.y + dx * sin(za) + dy * cos(za) * cos(xa);
  this.cameraLocation.z = this.cameraLocation.z + dy * sin(xa);
};

module.exports = Camera;
