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

package renderer.pipeline;

import renderer.scene.*;
import renderer.scene.primitives.LineSegment;
import renderer.framebuffer.*;
import static renderer.pipeline.PipelineLogger.*;

import java.awt.Color;

/**
   Rasterize a clipped {@link LineSegment} into shaded pixels
   in a {@link FrameBuffer.Viewport} and (optionally)
   anti-alias and gamma-encode the line at the same time.
<p>
   This pipeline stage takes a clipped {@link LineSegment}
   with vertices in the {@link Camera}'s view rectangle and
   rasterizezs the line segment into shaded, anti-aliased
   pixels in a {@link FrameBuffer}'s viewport. This rasterizer
   will linearly interpolate color from the line segment's two
   endpoints to each rasterized (and anti-aliased) pixel in
   the line segment.
<p>
   This rasterization algorithm is based on
<pre>
     "Fundamentals of Computer Graphics", 3rd Edition,
      by Peter Shirley, pages 163-165.
</pre>
<p>
   This rasterizer implements a simple version of Xiaolin_Wu's
   anti-aliasing algorithm. See
     <a href="https://en.wikipedia.org/wiki/Xiaolin_Wu's_line_algorithm" target="_top">
              https://en.wikipedia.org/wiki/Xiaolin_Wu's_line_algorithm</a>
*/
public final class Rasterize_AntiAlias_Line
{
   /**
      Rasterize a clipped {@link LineSegment} into anti-aliased, shaded pixels
      in the {@link FrameBuffer.Viewport}.

      @param model  {@link Model} that the {@link LineSegment} {@code ls} comes from
      @param ls     {@link LineSegment} to rasterize into the {@link FrameBuffer.Viewport}
      @param vp     {@link FrameBuffer.Viewport} to hold rasterized pixels
   */
   public static void rasterize(final Model model,
                                final LineSegment ls,
                                final FrameBuffer.Viewport vp)
   {
      final String     CLIPPED = "Clipped: ";
      final String NOT_CLIPPED = "         ";

      // Get the viewport's background color.
      final Color bg = vp.bgColorVP;

      // Make local copies of several values.
      final int w = vp.getWidthVP(),
                h = vp.getHeightVP();

      final int vIndex0 = ls.vIndexList.get(0),
                vIndex1 = ls.vIndexList.get(1);
      final Vertex v0 = model.vertexList.get(vIndex0),
                   v1 = model.vertexList.get(vIndex1);

      final int cIndex0 = ls.cIndexList.get(0),
                cIndex1 = ls.cIndexList.get(1);
      final float[] c0 = model.colorList.get(cIndex0).getRGBComponents(null),
                    c1 = model.colorList.get(cIndex1).getRGBComponents(null);
      float r0 = c0[0], g0 = c0[1], b0 = c0[2],
            r1 = c1[0], g1 = c1[1], b1 = c1[2];

      // Solve the "dangling edge" problem.
      double x0_pp = ((v0.x - 0.5) / 1.0001) + 0.5,
             y0_pp = ((v0.y - 0.5) / 1.0001) + 0.5,
             x1_pp = ((v1.x - 0.5) / 1.0001) + 0.5,
             y1_pp = ((v1.y - 0.5) / 1.0001) + 0.5;
      // This is explained on page 142 of
      //    "Jim Blinn's Corner: A Trip Down The Graphics Pipeline"
      //     by Jim Blinn, 1996, Morgan Kaufmann Publishers.

      // Round each vertex to the nearest logical pixel. This
      // makes the algorithm a lot simpler, but it can cause
      // a slight, but noticeable, shift of the line segment.
      x0_pp = Math.round(x0_pp);  // "snap-to-pixel"
      y0_pp = Math.round(y0_pp);
      x1_pp = Math.round(x1_pp);  // "snap-to-pixel"
      y1_pp = Math.round(y1_pp);

      if (Rasterize.debug)
      {
         logMessage(String.format("Snapped to (x0_pp, y0_pp) = (%9.4f, %9.4f)", x0_pp,y0_pp));
         logMessage(String.format("Snapped to (x1_pp, y1_pp) = (%9.4f, %9.4f)", x1_pp,y1_pp));
      }

      // Rasterize a degenerate line segment (a line segment
      // that projected onto a single point) as a single pixel.
      if ( (x0_pp == x1_pp) && (y0_pp == y1_pp) )
      {
         // We don't know which endpoint of the line segment
         // is in front, so just pick v0.
         final int x0_vp = (int)x0_pp - 1;  // viewport coordinate
         final int y0_vp = h - (int)y0_pp;  // viewport coordinate

         if (Rasterize.debug)
         {
            final String clippedMessage = NOT_CLIPPED;
            logPixel(clippedMessage, x0_pp, y0_pp, x0_vp, y0_vp, r0, g0, b0, vp);
         }
         // Log the pixel before setting it so that an array out-
         // of-bounds error will be right after the pixel's address.

         vp.setPixelVP(x0_vp, y0_vp, new Color(r0, g0, b0));

         return;
      }

      // If abs(slope) > 1, then transpose this line so that
      // the transposed line has slope < 1. Remember that the
      // line has been transposed.
      boolean transposedLine = false;
      if (Math.abs(y1_pp - y0_pp) > Math.abs(x1_pp - x0_pp)) // if abs(slope) > 1
      {
         // Swap x0_pp with y0_pp.
         final double temp0 = x0_pp;
         x0_pp = y0_pp;
         y0_pp = temp0;
         // Swap x1_pp with y1_pp.
         final double temp1 = x1_pp;
         x1_pp = y1_pp;
         y1_pp = temp1;
         transposedLine = true; // Remember that this line is transposed.
      }

      boolean swapped = false;
      if (x1_pp < x0_pp) // We want to rasterize in the direction of increasing x,
      {                  // so, if necessary, swap (x0, y0) with (x1, y1).
         final double tempX = x0_pp;
         final double tempY = y0_pp;
         x0_pp = x1_pp;
         y0_pp = y1_pp;
         x1_pp = tempX;
         y1_pp = tempY;
         // Swap the colors too.
         final float tempR = r0;
         final float tempG = g0;
         final float tempB = b0;
         r0 = r1;
         g0 = g1;
         b0 = b1;
         r1 = tempR;
         g1 = tempG;
         b1 = tempB;
         swapped = true;
      }

      // Compute this line segment's slopes.
      final double m = (y1_pp - y0_pp) / (x1_pp - x0_pp);
      final double slopeR =  (r1 - r0) / (x1_pp - x0_pp);
      final double slopeG =  (g1 - g0) / (x1_pp - x0_pp);
      final double slopeB =  (b1 - b0) / (x1_pp - x0_pp);

      if (Rasterize.debug)
      {
         final String inverseSlope = (transposedLine)
                                        ? " (transposed, so 1/m = " + 1/m + ")"
                                        : "";
         logMessage("Rasterize along the "
                     + (transposedLine?"y":"x") + "-axis in the "
                     + (swapped?"reversed":"original") + " direction.");
         logMessage("Slope m    = " + m + inverseSlope);
         logMessage("Slope mRed = " + slopeR);
         logMessage("Slope mGrn = " + slopeG);
         logMessage("Slope mBlu = " + slopeB);
         logMessage(String.format("Start at (x0_vp, y0_vp) = (%9.4f, %9.4f)", x0_pp-1,h-y0_pp));
         logMessage(String.format("  End at (x1_vp, y1_vp) = (%9.4f, %9.4f)", x1_pp-1,h-y1_pp));
      }

      // Rasterize this line segment in the direction of increasing x.
      // In the following loop, as x moves across the logical horizontal
      // or vertical pixels, we will compute a y value for each x.
      double y_pp = y0_pp;
      for (int x_pp = (int)x0_pp; x_pp < (int)x1_pp; x_pp += 1, y_pp += m)
      {
         // Interpolate this pixel's color between the two endpoint's colors.
         float r = (float)Math.abs(r0 + slopeR*(x_pp - x0_pp));
         float g = (float)Math.abs(g0 + slopeG*(x_pp - x0_pp));
         float b = (float)Math.abs(b0 + slopeB*(x_pp - x0_pp));
         // We need the Math.abs() because otherwise, we sometimes get -0.0.

         // Calculate the anti-aliased pixel colors.
         // y must be between two vertical or horizontal logical pixel
         //  coordinates. Let y_pp_low and y_pp_hi be the logical pixel
         // coordinates that bracket around y.
         final int y_pp_low = (int)Math.floor(y_pp);
               int y_pp_hi  = y_pp_low + 1;
         if (!transposedLine && y_pp == h) y_pp_hi = h; // test for the top edge
         if ( transposedLine && y_pp == w) y_pp_hi = w; // test for the right edge

         // Let weight be the distance from y to its floor (when
         // y is positive, this is the fractional part of y). We
         // will use weight to determine how much emphasis to place
         // on each of the two logical-pixels that bracket y.
         final float weight = (float)(y_pp - y_pp_low);

         // Interpolate colors for the low and high pixels.
         // The smaller weight is, the closer y is to the lower
         // pixel, so we give the lower pixel more emphasis when
         // weight is small.
         float r_low = (1 - weight) * r + weight * (bg.getRed()  /255.0f);
         float g_low = (1 - weight) * g + weight * (bg.getGreen()/255.0f);
         float b_low = (1 - weight) * b + weight * (bg.getBlue() /255.0f);
         float r_hi  = weight * r + (1 - weight) * (bg.getRed()  /255.0f);
         float g_hi  = weight * g + (1 - weight) * (bg.getGreen()/255.0f);
         float b_hi  = weight * b + (1 - weight) * (bg.getBlue() /255.0f);

         // The anti-aliasing is done in "linear color space". If gamma-
         // encoding is turned on, it is done after the anti-aliasing.
         if (Rasterize.doGamma)
         {
            // Apply gamma-encoding (gamma-compression) to the colors.
            // https://www.scratchapixel.com/lessons/digital-imaging/digital-images
            // http://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/
            final double gammaInv = 1.0 / Rasterize.GAMMA;
            r     = (float)Math.pow(r,     gammaInv);
            g     = (float)Math.pow(g,     gammaInv);
            b     = (float)Math.pow(b,     gammaInv);
            r_low = (float)Math.pow(r_low, gammaInv);
            g_low = (float)Math.pow(g_low, gammaInv);
            b_low = (float)Math.pow(b_low, gammaInv);
            r_hi  = (float)Math.pow(r_hi,  gammaInv);
            g_hi  = (float)Math.pow(g_hi,  gammaInv);
            b_hi  = (float)Math.pow(b_hi,  gammaInv);
         }

         if (Rasterize.doAntiAliasing)
         {
            setPixelAA(x_pp, y_pp,
                       r_low, g_low, b_low,
                       r_hi,  g_hi,  b_hi,
                       transposedLine,
                       vp);
         }
         else // No anti-aliasing.
         {
            setPixel(x_pp, y_pp, r, g, b, transposedLine, vp);
         }
         // Advance (x,y) to the next pixel (delta_x is 1, so delta_y is m).
      }
      // Set the pixel for the (x1_pp, y1_pp) endpoint.
      // We do this separately to avoid rounding errors.
      setPixel((int)x1_pp, y1_pp, r1, g1, b1, transposedLine, vp);
   }


   /**
      Local helper function for setting and
      logging pixels in the viewport.
   */
   private static void setPixel(int    x_pp,  // independent variable
                                double y_pp,  //   dependent variable
                                float r, float g, float b,
                                boolean transposedLine,
                                FrameBuffer.Viewport vp)
   {
      final String NOT_CLIPPED = "         ";
      final int h = vp.getHeightVP();

      // Calculate the viewport coordinates of the pixel.
      // The value of y will almost always be between
      // two vertical (or horizontal) pixel coordinates.
      // By rounding off the value of y, we are choosing the
      // nearest logical vertical (or horizontal) pixel coordinate.
      final int x_vp;  // viewport coordinate
      final int y_vp;  // viewport coordinate
      if ( ! transposedLine )
      {
         x_vp = x_pp - 1;
         y_vp = h - (int)Math.round(y_pp);
      }
      else
      {
         x_vp = (int)Math.round(y_pp) - 1;
         y_vp = h - x_pp;
      }

      if (Rasterize.debug)
      {
         final String clippedMessage = NOT_CLIPPED;
         if ( ! transposedLine )
         {
            logPixel(clippedMessage,
                     x_pp, y_pp,
                     x_vp, y_vp,
                     r, g, b,
                     vp);
         }
         else
         {
            logPixel(clippedMessage,
                     y_pp, x_pp,
                     x_vp, y_vp,
                     r, g, b,
                     vp);
         }
      }
      // Log the pixel before setting it so that an array out-
      // of-bounds error will be right after the pixel's address.

      vp.setPixelVP(x_vp, y_vp, new Color(r, g, b));
   }


   /**
      Local helper function for setting and logging
      anti-aliasedpixels in the viewport.
   */
   private static void setPixelAA(int    x_pp,  // independent variable
                                  double y_pp,  //   dependent variable
                                  float r_low,  float g_low,  float b_low,
                                  float r_hi,   float g_hi,   float b_hi,
                                  boolean transposedLine,
                                  FrameBuffer.Viewport vp)
   {
      final String NOT_CLIPPED = "         ";
      final int w = vp.getWidthVP(),
                h = vp.getHeightVP();

      // Calculate the viewport coordinates of the pixel.
      // y must be between two vertical or horizontal logical pixel
      // coordinates. Let y_pp_low and y_pp_hi be the logical pixel
      // coordinates that bracket around y.
      final int y_pp_low = (int)Math.floor(y_pp);
            int y_pp_hi  = y_pp_low + 1;
      if (!transposedLine && y_pp == h) y_pp_hi = h; // test for the top edge
      if ( transposedLine && y_pp == w) y_pp_hi = w; // test for the right edge

      final int x_vp;      // viewport coordinate
      final int y_vp_low;  // viewport coordinate
      final int y_vp_hi;   // viewport coordinates
      if ( ! transposedLine )
      {
         x_vp     = x_pp - 1;
         y_vp_low = h - y_pp_low;
         y_vp_hi  = h - y_pp_hi;
      }
      else
      {
         x_vp     = h - x_pp;
         y_vp_low = y_pp_low - 1;
         y_vp_hi  = y_pp_hi  - 1;
      }

      if (Rasterize.debug)
      {
         final String clippedMessage = NOT_CLIPPED;
         if ( ! transposedLine )
         {
            logPixelsAA(clippedMessage,
                        x_pp, y_pp,
                        x_vp, y_vp_low, y_vp_hi,
                        r_low, g_low, b_low,
                        r_hi,  g_hi,  b_hi,
                        vp);
         }
         else
         {
            logPixelsAA(clippedMessage,
                        y_pp, x_pp,
                        x_vp, y_vp_low, y_vp_hi,
                        r_low, g_low, b_low,
                        r_hi,  g_hi,  b_hi,
                        vp);
         }
      }
      // Log the pixel before setting it so that an array out-
      // of-bounds error will be right after the pixel's address.

      if ( ! transposedLine )
      {
         vp.setPixelVP(x_vp, y_vp_low, new Color(r_low, g_low, b_low));
         vp.setPixelVP(x_vp, y_vp_hi,  new Color(r_hi,  g_hi,  b_hi));
      }
      else
      {
         vp.setPixelVP(y_vp_low, x_vp, new Color(r_low, g_low, b_low));
         vp.setPixelVP(y_vp_hi,  x_vp, new Color(r_hi,  g_hi,  b_hi));
      }
   }


   // Private default constructor to enforce noninstantiable class.
   // See Item 4 in "Effective Java", 3rd Ed, Joshua Bloch.
   private Rasterize_AntiAlias_Line() {
      throw new AssertionError();
   }
}
