/*
 * Copyright 2009, Google Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


/**
 * @fileoverview This file contains sample code for controlling the camera
 * (ie view matrix) using the mouse and keyboard.
 */

o3djs.provide('o3djs.cameracontroller');

o3djs.require('o3djs.math');

/**
 * A Module for user control of the camera / view matrix.
 * @namespace
 */
o3djs.cameracontroller = o3djs.cameracontroller || {};

/**
 * The possible modes that a CameraController can be in.
 * One of these is usually set when a mouse button is pressed down,
 * and then NONE is set when the mouse button is released.
 * When the mouse is moved, the DragMode determines what effect the mouse move
 * has on the camera parameters (such as position and orientation).
 * If the DragMode is NONE, mouse moves have no effect.
 * @enum {number}
 */
o3djs.cameracontroller.DragMode = {
  /**
   * Dragging the mouse has no effect.
   */
  NONE: 0,
  /**
   * Dragging left or right changes rotationAngle,
   * dragging up or down changes heightAngle.
   */
  SPIN_ABOUT_CENTER: 1,
  /**
   * Dragging up or down changes the backpedal.
   */
  DOLLY_IN_OUT: 2,
  /**
   * Dragging up or down changes the fieldOfViewAngle.
   */
  ZOOM_IN_OUT: 3,
  /**
   * Dragging up or down changes the amount of perspective.
   * Perspective is focused on the centerPos.
   * If backpedal is negative or zero, there is no effect.
   */
  DOLLY_ZOOM: 4,
  /**
   * Dragging moves the centerPos around the plane perpendicular to
   * the camera view direction.
   */
  MOVE_CENTER_IN_VIEW_PLANE: 5,
};

/**
 * Creates a CameraController.
 * @param {!o3djs.math.Vector3} centerPos The position that the camera is
 *     looking at and rotating around; or if backpedal is zero, the location
 *     of the camera. In world space.
 * @param {number} backpedal The distance the camera moves back from the
 *     centerPos.
 * @param {number} heightAngle The angle the camera rotates up or down
 *     (about the x axis that passes through the centerPos). In radians.
 * @param {number} rotationAngle The angle the camera rotates left or right
 *     (about the y axis that passes through the centerPos). In radians.
 * @param {number} fieldOfViewAngle The vertical angle of the viewing frustum.
 *     In radians, between 0 and PI/2. This does not affect the view matrix,
 *     but it can still be useful to let the CameraController control the
 *     field of view.
 * @param {function(!o3djs.cameracontroller.CameraController): void}
 *     opt_onChange Pointer to a callback to call when the camera changes.
 * @return {!o3djs.cameracontroller.CameraController} The created
 *     CameraController.
 */
o3djs.cameracontroller.createCameraController = function(centerPos,
                                                         backpedal,
                                                         heightAngle,
                                                         rotationAngle,
                                                         fieldOfViewAngle,
                                                         opt_onChange) {
  return new o3djs.cameracontroller.CameraController(centerPos,
                                                     backpedal,
                                                     heightAngle,
                                                     rotationAngle,
                                                     fieldOfViewAngle,
                                                     opt_onChange);
};

/**
 * Class to hold user-controlled camera information and handle user events.
 * It can control and output a view matrix, and can also control some aspects
 * of a projection matrix.
 *
 * Most of the parameters it controls affect the view matrix, and it can
 * generate a view matrix based on its parameters.
 * It can also control certain parameters that affect the projection matrix,
 * such as field of view. Rather than deal with all the parameters needed for
 * a projection matrix, this class leaves generation of the projection matrix
 * up to the user code, and simply exposes the parameters it has.
 * @constructor
 * @param {!o3djs.math.Vector3} centerPos The position that the camera is
 *     looking at and rotating around; or if backpedal is zero, the location
 *     of the camera. In world space.
 * @param {number} backpedal The distance the camera moves back from the
 *     centerPos.
 * @param {number} heightAngle The angle the camera rotates up or down
 *     (about the x axis that passes through the centerPos). In radians.
 * @param {number} rotationAngle The angle the camera rotates left or right
 *     (about the y axis that passes through the centerPos). In radians.
 * @param {number} fieldOfViewAngle The vertical angle of the viewing frustum.
 *     In radians, between 0 and PI/2. This does not affect the view matrix,
 *     but it can still be useful to let this class control the field of view.
 * @param {function(!o3djs.cameracontroller.CameraController): void}
 *     opt_onChange Pointer to a callback to call when the camera changes.
 */
o3djs.cameracontroller.CameraController = function(centerPos,
                                                   backpedal,
                                                   heightAngle,
                                                   rotationAngle,
                                                   fieldOfViewAngle,
                                                   opt_onChange) {
  /**
   * The position that the camera is looking at and rotating around.
   * Or if backpedal is zero, the location of the camera. In world space.
   * @type {!o3djs.math.Vector3}
   */
  this.centerPos = centerPos;

  /**
   * The distance the camera moves back from the centerPos.
   * @type {number}
   */
  this.backpedal = backpedal;

  /**
   * The angle the camera rotates up or down.
   * @type {number}
   */
  this.heightAngle = heightAngle;

  /**
   * The angle the camera rotates left or right.
   * @type {number}
   */
  this.rotationAngle = rotationAngle;

  /**
   * The vertical angle of the perspective viewing frustum.
   * In radians, between 0 and PI/2. This does not affect the view matrix.
   * The user code can access this value and use it to construct a
   * projection matrix, or it can simply ignore it.
   * @type {number}
   */
  this.fieldOfViewAngle = fieldOfViewAngle;


  /**
   * Points to a callback to call when the camera changes.
   * @type {function(!o3djs.cameracontroller.CameraController): void}
   */
  this.onChange = opt_onChange || null;

  /**
   * The current mouse-drag mode, ie what happens when you move the mouse.
   * @private
   * @type {o3djs.cameracontroller.DragMode}
   */
  this.dragMode_ = o3djs.cameracontroller.DragMode.NONE;

  /**
   * The last X coordinate of the mouse.
   * @private
   * @type {number}
   */
  this.mouseX_ = 0;

  /**
   * The last Y coordinate of the mouse.
   * @private
   * @type {number}
   */
  this.mouseY_ = 0;


  // Some variables to control how quickly the camera changes when you
  // move the mouse a certain distance. Feel free to modify these.
  // Mouse pixels are converted into arbitrary "units" (for lack of
  // a better term), and then "units" are converted into an angle,
  // or a distance, etc as the case may be.

  /**
   * Controls how quickly the mouse moves the camera (in general).
   * Used to convert pixels into "units".
   * @type {number}
   */
  this.pixelsPerUnit = 300.0;

  /**
   * Controls how quickly the mouse affects rotation angles.
   * Used to convert "units" into radians.
   * @type {number}
   */
  this.radiansPerUnit = 1.0;

  /**
   * Controls how quickly the mouse affects camera translation.
   * Used to convert "units" into world space units of distance.
   * @type {number}
   */
  this.distancePerUnit = 10.0;

  /**
   * Controls how quickly the mouse affects zooming.
   * Used to convert "units" into zoom factor.
   * @type {number}
   */
  this.zoomPerUnit = 1.0;
};

/**
 * Calculates the center point and backpedal which will make the
 * camera view the entire supplied bounding box, assuming a symmetric
 * perspective projection. The heightAngle and rotationAngle are
 * unchanged.
 * @param {!o3d.BoundingBox} The bounding box to enclose in the view
 * volume.
 * @param {number} aspectRatio The aspect ratio of the viewing plane.
 */
o3djs.cameracontroller.CameraController.prototype.viewAll =
  function(boundingBox,
           aspectRatio) {
  // Form a view matrix facing in the correct direction but whose
  // origin is at the center of the bounding box
  var minExtent = boundingBox.minExtent;
  var maxExtent = boundingBox.maxExtent;
  var centerPos = o3djs.math.divVectorScalar(
      o3djs.math.addVector(minExtent, maxExtent), 2.0);
  var viewMatrix = this.calculateViewMatrix_(centerPos, 0);
  var maxBackpedal = 0;
  var vertFOV = this.fieldOfViewAngle;
  var tanVertFOV = Math.tan(vertFOV);
  var horizFOV = Math.atan(aspectRatio * tanVertFOV);
  var tanHorizFOV = Math.tan(horizFOV);
  var extents = [minExtent, maxExtent];
  for (var zi = 0; zi < 2; zi++) {
    for (var yi = 0; yi < 2; yi++) {
      for (var xi = 0; xi < 2; xi++) {
        // Form world space vector of this corner
        var vec = [extents[xi][0], extents[yi][1], extents[zi][2], 1];
        // Transform by the temporary view matrix
        vec = o3djs.math.mulVectorMatrix(vec, viewMatrix);
        // Consider only points on the +z side of the origin
        if (vec[2] >= 0.0) {
          // Figure out the backpedal based on the horizontal and
          // vertical view angles, and the z coordinate of the
          // corner
          maxBackpedal = Math.max(maxBackpedal,
                                  vec[2] + vec[0] / tanHorizFOV);
          maxBackpedal = Math.max(maxBackpedal,
                                  vec[2] + vec[1] / tanVertFOV);
        }
      }
    }
  }
  // Now set up the center point, backpedal and distancePerUnit
  this.centerPos = centerPos;
  this.backpedal = maxBackpedal;
  // This is heuristic based on some experimentation
  this.distancePerUnit = maxBackpedal / 5.0;
};

/**
 * Calculates the view matrix for this camera.
 * @return {!o3djs.math.Matrix4} The view matrix.
 */
o3djs.cameracontroller.CameraController.prototype.calculateViewMatrix =
    function() {
  return this.calculateViewMatrix_(this.centerPos, this.backpedal);
};

/**
 * Calculates the view matrix for this camera given the specified
 * center point and backpedal.
 * @param {!o3djs.math.Vector3} centerPoint Center point for the
 * camera.
 * @param {number} backpedal Backpedal from the center point for the
 * camera.
 */
o3djs.cameracontroller.CameraController.prototype.calculateViewMatrix_ =
    function(centerPoint, backpedal) {
  var matrix4 = o3djs.math.matrix4;
  var view = matrix4.translation(o3djs.math.negativeVector(centerPoint));
  view = matrix4.mul(view, matrix4.rotationY(this.rotationAngle));
  view = matrix4.mul(view, matrix4.rotationX(this.heightAngle));
  view = matrix4.mul(view, matrix4.translation([0, 0, -backpedal]));
  return view;
};

/**
 * Change the current mouse-drag mode, ie what happens when you move the mouse.
 * Usually you would set it to something when a mouse button is pressed down,
 * and then set it to NONE when the button is released.
 * @param {o3djs.cameracontroller.DragMode} dragMode The new DragMode.
 * @param {number} x The current mouse X coordinate.
 * @param {number} y The current mouse Y coordinate.
 */
o3djs.cameracontroller.CameraController.prototype.setDragMode =
    function(dragMode, x, y) {
  this.dragMode_ = dragMode;
  this.mouseX_ = x;
  this.mouseY_ = y;
};

/**
 * Method which should be called by end user code upon receiving a
 * mouse-move event.
 * @param {number} x The new mouse X coordinate.
 * @param {number} y The new mouse Y coordinate.
 */
o3djs.cameracontroller.CameraController.prototype.mouseMoved = function(x, y) {
  var deltaX = (x - this.mouseX_) / this.pixelsPerUnit;
  var deltaY = (y - this.mouseY_) / this.pixelsPerUnit;
  this.mouseX_ = x;
  this.mouseY_ = y;

  if (this.dragMode_ == o3djs.cameracontroller.DragMode.SPIN_ABOUT_CENTER) {
    this.rotationAngle += deltaX * this.radiansPerUnit;
    this.heightAngle += deltaY * this.radiansPerUnit;
  }
  if (this.dragMode_ == o3djs.cameracontroller.DragMode.DOLLY_IN_OUT) {
    this.backpedal += deltaY * this.distancePerUnit;
  }
  if (this.dragMode_ == o3djs.cameracontroller.DragMode.ZOOM_IN_OUT) {
    var width = Math.tan(this.fieldOfViewAngle);
    width *= Math.pow(2, deltaY * this.zoomPerUnit);
    this.fieldOfViewAngle = Math.atan(width);
  }
  if (this.dragMode_ == o3djs.cameracontroller.DragMode.DOLLY_ZOOM) {
    if (this.backpedal > 0) {
      var oldWidth = Math.tan(this.fieldOfViewAngle);
      this.fieldOfViewAngle += deltaY * this.radiansPerUnit;
      this.fieldOfViewAngle = Math.min(this.fieldOfViewAngle, 0.98 * Math.PI/2);
      this.fieldOfViewAngle = Math.max(this.fieldOfViewAngle, 0.02 * Math.PI/2);
      var newWidth = Math.tan(this.fieldOfViewAngle);
      this.backpedal *= oldWidth / newWidth;
    }
  }
  if (this.dragMode_ ==
      o3djs.cameracontroller.DragMode.MOVE_CENTER_IN_VIEW_PLANE) {
    var matrix4 = o3djs.math.matrix4;
    var translationVector = [-deltaX * this.distancePerUnit,
                              deltaY * this.distancePerUnit, 0];
    var inverseViewMatrix = matrix4.inverse(this.calculateViewMatrix());
    translationVector = matrix4.transformDirection(
        inverseViewMatrix, translationVector);
    this.centerPos = o3djs.math.addVector(this.centerPos, translationVector);
  }

  if (this.onChange != null &&
      this.dragMode_ != o3djs.cameracontroller.DragMode.NONE) {
    this.onChange(this);
  }
};