/*
 * Renderer 5. The MIT License.
 * Copyright (c) 2022 rlkraft@pnw.edu
 * See LICENSE for details.
*/

package renderer.scene;

/**
   This {@code Camera} data structure represents a camera
   located at the origin, looking down the negative z-axis,
   with a near clipping plane.
<p>
   This {@code Camera} has a configurable "view volume" that
   determines what part of space the camera "sees" when we use
   the camera to take a picture (that is, when we render a
   {@link Scene}).
<p>
   This {@code Camera} can "take a picture" two ways, using
   a perspective projection or a parallel (orthographic)
   projection. Each way of taking a picture has a different
   shape for its view volume. The data in this data structure
   determines the shape of each of the two view volumes.
<p>
   For the perspective projection, the view volume (in view
   coordinates!) is the infinitely long frustum that is formed
   by cutting at the near clipping plane, {@code z = -near},
   the infinitely long pyramid with its apex at the origin
   and its cross section in the image plane, {@code z = -1},
   with edges {@code x = -1}, {@code x = +1}, {@code y = -1},
   and {@code y = +1}. The perspective view volume's shape is
   set by the {@link projPerspective} method.
<p>
   For the orthographic projection, the view volume (in view
   coordinates!) is the semi-infinite rectangular cylinder
   parallel to the z-axis, with base in the near clipping plane,
   {@code z = -near}, and with edges {@code x = left},
   {@code x = right}, {@code y = bottom}, {@code y = top} (a
   semi-infinite parallelepiped). The orthographic view volume's
   shape is set by the {@link projOrtho} method.
<p>
   When the graphics rendering {@link renderer.pipeline.Pipeline}
   uses this {@code Camera} to render a {@link Scene}, the renderer
   only "sees" the geometry from the scene that is contained in this
   camera's view volume. (Notice that this means the orthographic
   camera can see geometry that is behind the camera. In fact, the
   perspective camera can also sees geometry that is behind the
   camera.) The renderer's {@link renderer.pipeline.NearClip} and
   {@link renderer.pipeline.Clip} pipeline stages are responsible
   for making sure that the scene's geometry that is outside of
   this camera's view volume is not visible.
<p>
   The plane {@code z = -1} (in view coordinates) is the camera's
   "image plane". The rectangle in the image plane with corners
   {@code (left, bottom, -1)} and {@code (right, top, -1)} is the
   camera's "view rectangle". The view rectangle is like the film
   in a real camera, it is where the camera's image appears when you
   take a picture. The contents of the camera's view rectangle (after
   it gets "normalized" to camera coordinates by the renderer's
   {@link renderer.pipeline.View2Camera} stage) is what gets rasterized,
   by the renderer's {@link renderer.pipeline.Rasterize}
   pipeline stage, into a {@link renderer.framebuffer.FrameBuffer}'s
   {@link renderer.framebuffer.FrameBuffer.Viewport}.
<p>
   For both the perspective and the parallel projections, the camera's
   near plane is there to prevent the camera from seeing what is "behind"
   the near plane. For the perspective projection, the near plane also
   prevents the renderer from incorrectly rasterizing line segments that
   cross the camera plane, {@code z = 0}.
*/
public final class Camera
{
   // Choose either perspective or parallel projection.
   public final boolean perspective;
   // The following five numbers define the camera's view volume.
   public final double left;
   public final double right;
   public final double bottom;
   public final double top;
   public final double n;  // near clipping plane

   /**
      A private {@code Camera} constructor for
      use by the static factory methods.
   */
   private Camera(final boolean perspective,
                  final double left,
                  final double right,
                  final double bottom,
                  final double top,
                  final double n)
   {
      this.perspective = perspective;
      this.left = left;
      this.right = right;
      this.bottom = bottom;
      this.top = top;
      this.n = n;
   }


   /**
      This is a static factory method.
      <p>
      Set up this {@code Camera}'s view volume as a perspective projection
      of the normalized infinite view pyramid extending along the negative
      z-axis.

      @return a new {@code Camera} object with the default perspective parameters
   */
   public static Camera projPerspective()
   {
      return projPerspective(-1.0, +1.0, -1.0, +1.0); // left, right, bottom, top
   }


   /**
      This is a static factory method.
      <p>
      Set up this {@code Camera}'s view volume as a perspective projection
      of an infinite view pyramid extending along the negative z-axis.

      @param left    left edge of view rectangle in the image plane
      @param right   right edge of view rectangle in the image plane
      @param bottom  bottom edge of view rectangle in the image plane
      @param top     top edge of view rectangle in the image plane
      @return a new {@code Camera} object with the given parameters
   */
   public static Camera projPerspective(final double left,   final double right,
                                        final double bottom, final double top)
   {
      return projPerspective(left, right, bottom, top, 1.0);
   }


   /**
      This is a static factory method.
      <p>
      Set up this {@code Camera}'s view volume as a perspective projection
      of an infinite view pyramid extending along the negative z-axis.
      <p>
      Use {@code focalLength} to determine the image plane. So the
      {@code left}, {@code right}, {@code bottom}, {@code top}
      parameters are used in the plane {@code z = -focalLength}.
      <p>
      The {@code focalLength} parameter can be used to zoom an
      asymmetric view volume, much like the {@code fovy} parameter
      for the symmetric view volume, or the "near" parameter for
      the OpenGL gluPerspective() function.

      @param left    left edge of view rectangle in the image plane
      @param right   right edge of view rectangle in the image plane
      @param bottom  bottom edge of view rectangle in the image plane
      @param top     top edge of view rectangle in the image plane
      @param focalLength  distance from the origin to the image plane
      @return a new {@code Camera} object with the given parameters
   */
   public static Camera projPerspective(final double left,   final double right,
                                        final double bottom, final double top,
                                        final double focalLength)
   {
      return new Camera(true,
                        left / focalLength,
                        right / focalLength,
                        bottom / focalLength,
                        top / focalLength,
                        -0.1);  // near clipping plane (near = +0.1)
   }


   /**
      This is a static factory method.
      <p>
      Set up this {@code Camera}'s view volume as a symmetric infinite
      view pyramid extending along the negative z-axis.
      <p>
      Here, the view volume is determined by a vertical "field of view"
      angle and an aspect ratio for the view rectangle in the image plane.

      @param fovy    angle in the y-direction subtended by the view rectangle in the image plane
      @param aspect  aspect ratio of the view rectangle in the image plane
      @return a new {@code Camera} object with the given parameters
   */
   public static Camera projPerspective(final double fovy, final double aspect)
   {
      final double top    =  Math.tan((Math.PI/180.0)*fovy/2.0);
      final double bottom = -top;
      final double right  =  top * aspect;
      final double left   = -right;

      return projPerspective(left, right, bottom, top);
   }


   /**
      This is a static factory method.
      <p>
      Set up this {@code Camera}'s view volume as a parallel (orthographic)
      projection of the normalized infinite view parallelepiped extending
      along the z-axis.

      @return a new {@code Camera} object with the default orthographic parameters
   */
   public static Camera projOrtho()
   {
      return projOrtho(-1.0, +1.0, -1.0, +1.0); // left, right, bottom, top
   }


   /**
      This is a static factory method.
      <p>
      Set up this {@code Camera}'s view volume as a parallel (orthographic)
      projection of an infinite view parallelepiped extending along the
      z-axis.

      @param left    left edge of view rectangle in the xy-plane
      @param right   right edge of view rectangle in the xy-plane
      @param bottom  bottom edge of view rectangle in the xy-plane
      @param top     top edge of view rectangle in the xy-plane
      @return a new {@code Camera} object with the given parameters
   */
   public static Camera projOrtho(final double left,   final double right,
                                  final double bottom, final double top)
   {
      return new Camera(false,
                        left,
                        right,
                        bottom,
                        top,
                        +1.0);  // near clipping plane (near = -1.0)
   }


   /**
      This is a static factory method.
      <p>
      Set up this {@code Camera}'s view volume as a symmetric infinite
      view parallelepiped extending along the z-axis.
      <p>
      Here, the view volume is determined by a vertical "field-of-view"
      angle and an aspect ratio for the view rectangle in the image plane.

      @param fovy    angle in the y-direction subtended by the view rectangle in the image plane
      @param aspect  aspect ratio of the view rectangle in the image plane
      @return a new {@code Camera} object with the given parameters
   */
   public static Camera projOrtho(final double fovy, final double aspect)
   {
      final double top    =  Math.tan((Math.PI/180.0)*fovy/2.0);
      final double bottom = -top;
      final double right  =  top * aspect;
      final double left   = -right;

      return projOrtho(left, right, bottom, top);
   }


   /**
      Create a new {@code Camera} that is essentially the same as this
      {@code Camera} but with the given distance from the camera to
      the near clipping plane.
      <p>
      When {@code near} is positive, the near clipping plane is in
      front of the camera. When {@code near} is negative, the near
      clipping plane is behind the camera.

      @param near  distance from the new {@code Camera} to its near clipping plane
      @return a new {@code Camera} object with the given value for near
   */
   public Camera changeNear(final double near)
   {
      return new Camera(this.perspective,
                        this.left,
                        this.right,
                        this.bottom,
                        this.top,
                        -near);
   }


   /**
      For debugging.

      @return {@link String} representation of this {@code Camera} object
   */
   public String toString()
   {
      final double fovy = (180./Math.PI) * Math.atan(top)
                        + (180./Math.PI) * Math.atan(-bottom);
      final double ratio = (right - left) / (top - bottom);
      String result = "";
      result += "Camera: \n";
      result += "  perspective = " + perspective + "\n";
      result += "  left = "   + left + ", "
             +  "  right = "  + right + "\n"
             +  "  bottom = " + bottom + ", "
             +  "  top = "    + top + "\n"
             +  "  near = "   + -n + "\n"
             +  "  (fovy = " + fovy
             +  ", aspect ratio = " + String.format("%.2f", ratio) + ")";
      return result;
   }
}
