// Copyright (c) 2015, Google Inc. Please see the AUTHORS file for details.
// All rights reserved. Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

part of vector_math_64;

/// Constructs a rotation matrix in [rotationMatrix].
///
/// Sets [rotationMatrix] to a rotation matrix built from
/// [forwardDirection] and [upDirection]. The right direction is
/// constructed to be orthogonal to [forwardDirection] and
/// [upDirection].
///
/// [forwardDirection] specifies the direction of the forward vector.
/// [upDirection] specifies the direction of the up vector.
///
/// Use case is to build the per-model rotation matrix from vectors
/// [forwardDirection] and [upDirection]. See sample code below for
/// a context.
///
///     class Model {
///       Vector3 _center = new Vector3.zero();        // per-model translation
///       Vector3 _scale = new Vector3(1.0, 1.0, 1.0); // per-model scaling
///       Matrix4 _rotation = new Matrix4.identity();  // per-model rotation
///       Matrix4 _MV = new Matrix4.identity();        // per-model model-view
///
///       void updateModelViewUniform(RenderingContext gl, UniformLocation u_MV,
///         Vector3 camPosition, camFocusPosition, camUpDirection) {
///
///         // V = View (inverse of camera)
///         // T = Translation
///         // R = Rotation
///         // S = Scaling
///         setViewMatrix(_MV, camPosition, camFocusPosition, camUpDirection); // MV = V
///         _MV.translate(_center); // MV = V*T
///         _MV.multiply(_rotation); // MV = V*T*R
///         // _rotation is updated with setRotationMatrix(_rotation, forward, up);
///         _MV.scale(_scale); // MV = V*T*R*S
///
///         gl.uniformMatrix4fv(u_MV, false, _MV.storage);
///       }
///     }
void setRotationMatrix(
    Matrix4 rotationMatrix, Vector3 forwardDirection, Vector3 upDirection) {
  setModelMatrix(rotationMatrix, forwardDirection, upDirection, 0.0, 0.0, 0.0);
}

/// Constructs an OpenGL model matrix in [modelMatrix].
/// Model transformation is the inverse of the view transformation.
/// Model transformation is also known as "camera" transformation.
/// Model matrix is commonly used to compute a object location/orientation into
/// the full model-view stack.
///
/// [forwardDirection] specifies the direction of the forward vector.
/// [upDirection] specifies the direction of the up vector.
/// [tx],[ty],[tz] specifies the position of the object.
void setModelMatrix(Matrix4 modelMatrix, Vector3 forwardDirection,
    Vector3 upDirection, double tx, double ty, double tz) {
  final Vector3 right = forwardDirection.cross(upDirection)..normalize();
  final Vector3 c1 = right;
  final Vector3 c2 = upDirection;
  final Vector3 c3 = -forwardDirection;
  modelMatrix.setValues(c1[0], c1[1], c1[2], 0.0, c2[0], c2[1], c2[2], 0.0,
      c3[0], c3[1], c3[2], 0.0, tx, ty, tz, 1.0);
}

/// Constructs an OpenGL view matrix in [viewMatrix].
/// View transformation is the inverse of the model transformation.
/// View matrix is commonly used to compute the camera location/orientation into
/// the full model-view stack.
///
/// [cameraPosition] specifies the position of the camera.
/// [cameraFocusPosition] specifies the position the camera is focused on.
/// [upDirection] specifies the direction of the up vector (usually, +Y).
void setViewMatrix(Matrix4 viewMatrix, Vector3 cameraPosition,
    Vector3 cameraFocusPosition, Vector3 upDirection) {
  final Vector3 z = (cameraPosition - cameraFocusPosition)..normalize();
  final Vector3 x = upDirection.cross(z)..normalize();
  final Vector3 y = z.cross(x)..normalize();

  final double rotatedEyeX = -x.dot(cameraPosition);
  final double rotatedEyeY = -y.dot(cameraPosition);
  final double rotatedEyeZ = -z.dot(cameraPosition);

  viewMatrix.setValues(x[0], y[0], z[0], 0.0, x[1], y[1], z[1], 0.0, x[2], y[2],
      z[2], 0.0, rotatedEyeX, rotatedEyeY, rotatedEyeZ, 1.0);
}

/// Constructs a new OpenGL view matrix.
///
/// [cameraPosition] specifies the position of the camera.
/// [cameraFocusPosition] specifies the position the camera is focused on.
/// [upDirection] specifies the direction of the up vector (usually, +Y).
Matrix4 makeViewMatrix(
    Vector3 cameraPosition, Vector3 cameraFocusPosition, Vector3 upDirection) {
  final Matrix4 r = new Matrix4.zero();
  setViewMatrix(r, cameraPosition, cameraFocusPosition, upDirection);
  return r;
}

/// Constructs an OpenGL perspective projection matrix in [perspectiveMatrix].
///
/// [fovYRadians] specifies the field of view angle, in radians, in the y
/// direction.
/// [aspectRatio] specifies the aspect ratio that determines the field of view
/// in the x direction. The aspect ratio of x (width) to y (height).
/// [zNear] specifies the distance from the viewer to the near plane
/// (always positive).
/// [zFar] specifies the distance from the viewer to the far plane
/// (always positive).
void setPerspectiveMatrix(Matrix4 perspectiveMatrix, double fovYRadians,
    double aspectRatio, double zNear, double zFar) {
  final double height = math.tan(fovYRadians * 0.5);
  final double width = height * aspectRatio;
  final double near_minus_far = zNear - zFar;

  perspectiveMatrix
    ..setZero()
    ..setEntry(0, 0, 1.0 / width)
    ..setEntry(1, 1, 1.0 / height)
    ..setEntry(2, 2, (zFar + zNear) / near_minus_far)
    ..setEntry(3, 2, -1.0)
    ..setEntry(2, 3, (2.0 * zNear * zFar) / near_minus_far);
}

/// Constructs a new OpenGL perspective projection matrix.
///
/// [fovYRadians] specifies the field of view angle, in radians, in the y
/// direction.
/// [aspectRatio] specifies the aspect ratio that determines the field of view
/// in the x direction. The aspect ratio of x (width) to y (height).
/// [zNear] specifies the distance from the viewer to the near plane
/// (always positive).
/// [zFar] specifies the distance from the viewer to the far plane
/// (always positive).
Matrix4 makePerspectiveMatrix(
    double fovYRadians, double aspectRatio, double zNear, double zFar) {
  final Matrix4 r = new Matrix4.zero();
  setPerspectiveMatrix(r, fovYRadians, aspectRatio, zNear, zFar);
  return r;
}

/// Constructs an OpenGL infinite projection matrix in [infiniteMatrix].
/// [fovYRadians] specifies the field of view angle, in radians, in the y
/// direction.
/// [aspectRatio] specifies the aspect ratio that determines the field of view
/// in the x direction. The aspect ratio of x (width) to y (height).
/// [zNear] specifies the distance from the viewer to the near plane
/// (always positive).
void setInfiniteMatrix(Matrix4 infiniteMatrix, double fovYRadians,
    double aspectRatio, double zNear) {
  final double height = math.tan(fovYRadians * 0.5);
  final double width = height * aspectRatio;

  infiniteMatrix
    ..setZero()
    ..setEntry(0, 0, 1.0 / width)
    ..setEntry(1, 1, 1.0 / height)
    ..setEntry(2, 2, -1.0)
    ..setEntry(3, 2, -1.0)
    ..setEntry(2, 3, -2.0 * zNear);
}

/// Constructs a new OpenGL infinite projection matrix.
///
/// [fovYRadians] specifies the field of view angle, in radians, in the y
/// direction.
/// [aspectRatio] specifies the aspect ratio that determines the field of view
/// in the x direction. The aspect ratio of x (width) to y (height).
/// [zNear] specifies the distance from the viewer to the near plane
/// (always positive).
Matrix4 makeInfiniteMatrix(
    double fovYRadians, double aspectRatio, double zNear) {
  final Matrix4 r = new Matrix4.zero();
  setInfiniteMatrix(r, fovYRadians, aspectRatio, zNear);
  return r;
}

/// Constructs an OpenGL perspective projection matrix in [perspectiveMatrix].
///
/// [left], [right] specify the coordinates for the left and right vertical
/// clipping planes.
/// [bottom], [top] specify the coordinates for the bottom and top horizontal
/// clipping planes.
/// [near], [far] specify the coordinates to the near and far depth clipping
/// planes.
void setFrustumMatrix(Matrix4 perspectiveMatrix, double left, double right,
    double bottom, double top, double near, double far) {
  final double two_near = 2.0 * near;
  final double right_minus_left = right - left;
  final double top_minus_bottom = top - bottom;
  final double far_minus_near = far - near;
  perspectiveMatrix
    ..setZero()
    ..setEntry(0, 0, two_near / right_minus_left)
    ..setEntry(1, 1, two_near / top_minus_bottom)
    ..setEntry(0, 2, (right + left) / right_minus_left)
    ..setEntry(1, 2, (top + bottom) / top_minus_bottom)
    ..setEntry(2, 2, -(far + near) / far_minus_near)
    ..setEntry(3, 2, -1.0)
    ..setEntry(2, 3, -(two_near * far) / far_minus_near);
}

/// Constructs a new OpenGL perspective projection matrix.
///
/// [left], [right] specify the coordinates for the left and right vertical
/// clipping planes.
/// [bottom], [top] specify the coordinates for the bottom and top horizontal
/// clipping planes.
/// [near], [far] specify the coordinates to the near and far depth clipping
/// planes.
Matrix4 makeFrustumMatrix(double left, double right, double bottom, double top,
    double near, double far) {
  final Matrix4 view = new Matrix4.zero();
  setFrustumMatrix(view, left, right, bottom, top, near, far);
  return view;
}

/// Constructs an OpenGL orthographic projection matrix in [orthographicMatrix].
///
/// [left], [right] specify the coordinates for the left and right vertical
/// clipping planes.
/// [bottom], [top] specify the coordinates for the bottom and top horizontal
/// clipping planes.
/// [near], [far] specify the coordinates to the near and far depth clipping
/// planes.
void setOrthographicMatrix(Matrix4 orthographicMatrix, double left,
    double right, double bottom, double top, double near, double far) {
  final double rml = right - left;
  final double rpl = right + left;
  final double tmb = top - bottom;
  final double tpb = top + bottom;
  final double fmn = far - near;
  final double fpn = far + near;
  orthographicMatrix
    ..setZero()
    ..setEntry(0, 0, 2.0 / rml)
    ..setEntry(1, 1, 2.0 / tmb)
    ..setEntry(2, 2, -2.0 / fmn)
    ..setEntry(0, 3, -rpl / rml)
    ..setEntry(1, 3, -tpb / tmb)
    ..setEntry(2, 3, -fpn / fmn)
    ..setEntry(3, 3, 1.0);
}

/// Constructs a new OpenGL orthographic projection matrix.
///
/// [left], [right] specify the coordinates for the left and right vertical
/// clipping planes.
/// [bottom], [top] specify the coordinates for the bottom and top horizontal
/// clipping planes.
/// [near], [far] specify the coordinates to the near and far depth clipping
/// planes.
Matrix4 makeOrthographicMatrix(double left, double right, double bottom,
    double top, double near, double far) {
  final Matrix4 r = new Matrix4.zero();
  setOrthographicMatrix(r, left, right, bottom, top, near, far);
  return r;
}

/// Returns a transformation matrix that transforms points onto
/// the plane specified with [planeNormal] and [planePoint].
Matrix4 makePlaneProjection(Vector3 planeNormal, Vector3 planePoint) {
  final Vector4 v = new Vector4(planeNormal.storage[0], planeNormal.storage[1],
      planeNormal.storage[2], 0.0);
  final Matrix4 outer = new Matrix4.outer(v, v);
  Matrix4 r = new Matrix4.zero();
  r = r - outer;
  final Vector3 scaledNormal =
      (planeNormal.scaled(dot3(planePoint, planeNormal)));
  final Vector4 T = new Vector4(scaledNormal.storage[0],
      scaledNormal.storage[1], scaledNormal.storage[2], 1.0);
  r.setColumn(3, T);
  return r;
}

/// Returns a transformation matrix that transforms points by reflecting
/// them through the plane specified with [planeNormal] and [planePoint].
Matrix4 makePlaneReflection(Vector3 planeNormal, Vector3 planePoint) {
  final Vector4 v = new Vector4(planeNormal.storage[0], planeNormal.storage[1],
      planeNormal.storage[2], 0.0);
  final Matrix4 outer = new Matrix4.outer(v, v)..scale(2.0);
  Matrix4 r = new Matrix4.zero();
  r = r - outer;
  final double scale = 2.0 * planePoint.dot(planeNormal);
  final Vector3 scaledNormal = (planeNormal.scaled(scale));
  final Vector4 T = new Vector4(scaledNormal.storage[0],
      scaledNormal.storage[1], scaledNormal.storage[2], 1.0);
  r.setColumn(3, T);
  return r;
}

/// On success, Sets [pickWorld] to be the world space position of
/// the screen space [pickX], [pickY], and [pickZ].
///
/// The viewport is specified by ([viewportX], [viewportWidth]) and
/// ([viewportY], [viewportHeight]).
///
/// [cameraMatrix] includes both the projection and view transforms.
///
/// [pickZ] is typically either 0.0 (near plane) or 1.0 (far plane).
///
/// Returns false on error, for example, the mouse is not in the viewport
bool unproject(
    Matrix4 cameraMatrix,
    num viewportX,
    num viewportWidth,
    num viewportY,
    num viewportHeight,
    num pickX,
    num pickY,
    num pickZ,
    Vector3 pickWorld) {
  viewportX = viewportX.toDouble();
  viewportWidth = viewportWidth.toDouble();
  viewportY = viewportY.toDouble();
  viewportHeight = viewportHeight.toDouble();
  pickX = pickX.toDouble();
  pickY = pickY.toDouble();
  pickX = (pickX - viewportX);
  pickY = (pickY - viewportY);
  pickX = (2.0 * pickX / viewportWidth) - 1.0;
  pickY = (2.0 * pickY / viewportHeight) - 1.0;
  pickZ = (2.0 * pickZ) - 1.0;

  // Check if pick point is inside unit cube
  if (pickX < -1.0 ||
      pickY < -1.0 ||
      pickX > 1.0 ||
      pickY > 1.0 ||
      pickZ < -1.0 ||
      pickZ > 1.0) {
    return false;
  }

  // Copy camera matrix.
  final Matrix4 invertedCameraMatrix = new Matrix4.copy(cameraMatrix);
  // Invert the camera matrix.
  invertedCameraMatrix.invert();
  // Determine intersection point.
  final Vector4 v =
      new Vector4(pickX.toDouble(), pickY.toDouble(), pickZ.toDouble(), 1.0);
  invertedCameraMatrix.transform(v);
  if (v.w == 0.0) {
    return false;
  }
  final double invW = 1.0 / v.w;
  pickWorld
    ..x = v.x * invW
    ..y = v.y * invW
    ..z = v.z * invW;

  return true;
}

/// On success, [rayNear] and [rayFar] are the points where
/// the screen space [pickX], [pickY] intersect with the near and far
/// planes respectively.
///
/// The viewport is specified by ([viewportX], [viewportWidth]) and
/// ([viewportY], [viewportHeight]).
///
/// [cameraMatrix] includes both the projection and view transforms.
///
/// Returns false on error, for example, the mouse is not in the viewport.
bool pickRay(
    Matrix4 cameraMatrix,
    num viewportX,
    num viewportWidth,
    num viewportY,
    num viewportHeight,
    num pickX,
    num pickY,
    Vector3 rayNear,
    Vector3 rayFar) {
  bool r;

  r = unproject(cameraMatrix, viewportX, viewportWidth, viewportY,
      viewportHeight, pickX, viewportHeight - pickY, 0.0, rayNear);
  if (!r) {
    return false;
  }

  r = unproject(cameraMatrix, viewportX, viewportWidth, viewportY,
      viewportHeight, pickX, viewportHeight - pickY, 1.0, rayFar);

  return r;
}
