/*
 * 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.*;
import static renderer.pipeline.PipelineLogger.*;

import java.awt.Color;
import java.util.Optional;

/**
   Clip a (projected) {@link LineSegment} that sticks out
   of the camera's view rectangle in the image plane.
   Interpolate {@link Vertex} color from any clipped off
   {@code Vertex} to the new {@code Vertex}.
<p>
   This clipping algorithm is a simplification of the Liang-Barsky
   Parametric Line Clipping algorithm.
<p>
   This algorithm assumes that all {@code Vertex} objects have been
   projected onto the {@link renderer.scene.Camera}'s image plane,
   {@code z = -1}. This algorithm also assumes that the camera's view
   rectangle in the image plane is
   <pre>{@code
      -1 <= x <= +1  and
      -1 <= y <= +1.
   }</pre>
*/
public class Clip_Line
{
   /**
      If the {@link LineSegment} sticks out of the view rectangle,
      then return a clipped version that is contained in the view
      rectangle. The new, clipped {@link LineSegment} object is
      returned wrapped in an {@link Optional} object.
      <p>
      At least one new clipped {@link Vertex} will be added to the
      {@link Model}'s vertex list (and as many as four new vertices
      may be added to the {@link Model}'s vertex list).
      <p>
      If the {@link LineSegment} is completely outside of the view
      rectangle, then return an empty {@link Optional} object to
      indicate that the {@link LineSegment} should be discarded.

      @param model  {@link Model} that the {@link LineSegment} {@code ls} comes from
      @param ls     {@link LineSegment} to be clipped
      @return a clipped version of {@code ls} wrapped in an {@link Optional} object
   */
   public static Optional<Primitive> clip(final Model model,
                                          final LineSegment ls)
   {
      // Make local copies of several values.
      final int vIndex0 = ls.vIndexList.get(0);
      final int vIndex1 = ls.vIndexList.get(1);
      final Vertex v0 = model.vertexList.get(vIndex0);
      final Vertex v1 = model.vertexList.get(vIndex1);

      final double x0 = v0.x,  y0 = v0.y;
      final double x1 = v1.x,  y1 = v1.y;

      // 1. Check for trivial accept.
      if ( Math.abs( x0 ) <= 1
        && Math.abs( y0 ) <= 1
        && Math.abs( x1 ) <= 1
        && Math.abs( y1 ) <= 1 )
      {
         if (Clip.debug) logMessage("-- Trivial accept.");
         return Optional.of(ls);  // better than "return ls"
      }
      // 2. Check for trivial delete.
      else if ( (x0 >  1 && x1 >  1)   // to the right of the line x = 1
             || (x0 < -1 && x1 < -1)   // to the left of the line x = -1
             || (y0 >  1 && y1 >  1)   // above the line y = 1
             || (y0 < -1 && y1 < -1) ) // below the line y = -1
      {
         if (Clip.debug) logMessage("-- Trivial delete.");
         return Optional.empty();  // better than "return null"
      }
      // 3. Need to clip this line segment.
      else
      {
         // Recursively clip a new (clipped) line segment.
         return clip(model, clipOneTime(model, ls));
      }
   }


   /**
      This method takes in a line segment that crosses one of the four
      clipping lines. This method returns a new LineSegment that uses
      a new, interpolated, Vertex that is the intersection point between
      the given LineSegment and the crossed clipping line.
      <p>
      This method solves for the value of {@code t} for which the
      parametric equation
      <pre>{@code
                  p(t) = (1-t) * v_outside + t * v_inside
      }</pre>
      intersects the crossed clipping line. (Notice that the equation
      is parameterized so that we move from the outside vertex towards
      the inside vertex as {@code t} increases from 0 to 1.) The solved
      for value of {@code t} is then plugged into the parametric formula
      to get the coordinates of the intersection point.
      <p>
      The new interpolated Vertex is added to the end of the vertex list
      from the given Model.

      @param model  {@link Model} that the {@link LineSegment} {@code ls} comes from
      @param ls     the {@link LineSegment} being clipped
      @return a new clipped {@link LineSegment} object
   */
   private static LineSegment clipOneTime(final Model model,
                                          final LineSegment ls)
   {
      // Make local copies of several values.
      final int vIndex0 = ls.vIndexList.get(0);
      final int vIndex1 = ls.vIndexList.get(1);
      final Vertex v0 = model.vertexList.get(vIndex0);
      final Vertex v1 = model.vertexList.get(vIndex1);

      final double x0 = v0.x,  y0 = v0.y;
      final double x1 = v1.x,  y1 = v1.y;

      final String equation;  // keep track of which edge is crossed
      final int    vOutside;  // keep track of which vertex is on the outside
      final double vOx, vOy;  // "O" for "outside"
      final double vIx, vIy;  // "I" for "inside"
      final double t;         // when we cross the clipping line
      final double x;         // x-coordinate of where we cross the clipping line
      final double y;         // y-coordinate of where we cross the clipping line
      final int vIndexNew;    // index for the new, interpolated, vertex

      if (x0 > 1)  // ls crosses the line x = 1
      {
         equation = "x = +1";
         vOutside = 0;
         vOx = x0;  vOy = y0;
         vIx = x1;  vIy = y1;
         t = (1 - vOx) / (vIx - vOx);
         x = 1;  // prevent rounding errors
         y = (1 - t) * vOy + t * vIy;
         final Vertex newVertex = new Vertex(x, y, 0);
         // Modify the Model to contain the new Vertex.
         vIndexNew = model.vertexList.size();
         model.vertexList.add(newVertex);
      }
      else if (x1 > 1)  // ls crosses the line x = 1
      {
         equation = "x = +1";
         vOutside = 1;
         vIx = x0;  vIy = y0;
         vOx = x1;  vOy = y1;
         t = (1 - vOx) / (vIx - vOx);
         x = 1;  // prevent rounding errors
         y = (1 - t) * vOy + t * vIy;
         final Vertex newVertex = new Vertex(x, y, 0);
         vIndexNew = model.vertexList.size();
         model.vertexList.add(newVertex);
      }
      else if (x0 < -1)  // ls crosses the line x = -1
      {
         equation = "x = -1";
         vOutside = 0;
         vOx = x0;  vOy = y0;
         vIx = x1;  vIy = y1;
         t = (-1 - vOx) / (vIx - vOx);
         x = -1;  // prevent rounding errors
         y = (1 - t) * vOy + t * vIy;
         final Vertex newVertex = new Vertex(x, y, 0);
         vIndexNew = model.vertexList.size();
         model.vertexList.add(newVertex);
      }
      else if (x1 < -1)  // ls crosses the line x = -1
      {
         equation = "x = -1";
         vOutside = 1;
         vIx = x0;  vIy = y0;
         vOx = x1;  vOy = y1;
         t = (-1 - vOx) / (vIx - vOx);
         x = -1;  // prevent rounding errors
         y = (1 - t) * vOy + t * vIy;
         final Vertex newVertex = new Vertex(x, y, 0);
         vIndexNew = model.vertexList.size();
         model.vertexList.add(newVertex);
      }
      else if (y0 > 1)  // ls crosses the line y = 1
      {
         equation = "y = +1";
         vOutside = 0;
         vOx = x0;  vOy = y0;
         vIx = x1;  vIy = y1;
         t = (1 - vOy) / (vIy - vOy);
         x = (1 - t) * vOx + t * vIx;
         y = 1;  // prevent rounding errors
         final Vertex newVertex = new Vertex(x, y, 0);
         vIndexNew = model.vertexList.size();
         model.vertexList.add(newVertex);
      }
      else if (y1 > 1)  // ls crosses the line y = 1
      {
         equation = "y = +1";
         vOutside = 1;
         vIx = x0;  vIy = y0;
         vOx = x1;  vOy = y1;
         t = (1 - vOy) / (vIy - vOy);
         x = (1 - t) * vOx + t * vIx;
         y = 1;  // prevent rounding errors
         final Vertex newVertex = new Vertex(x, y, 0);
         vIndexNew = model.vertexList.size();
         model.vertexList.add(newVertex);
      }
      else if (y0 < -1)  // ls crosses the line y = -1
      {
         equation = "y = -1";
         vOutside = 0;
         vOx = x0;  vOy = y0;
         vIx = x1;  vIy = y1;
         t = (-1 - vOy) / (vIy - vOy);
         x = (1 - t) * vOx + t * vIx;
         y = -1;  // prevent rounding errors
         final Vertex newVertex = new Vertex(x, y, 0);
         vIndexNew = model.vertexList.size();
         model.vertexList.add(newVertex);
      }
      else // if (y1 < -1)  // ls crosses the line y = -1
      {
         equation = "y = -1";
         vOutside = 1;
         vIx = x0;  vIy = y0;
         vOx = x1;  vOy = y1;
         t = (-1 - vOy) / (vIy - vOy);
         x = (1 - t) * vOx + t * vIx;
         y = -1;  // prevent rounding errors
         final Vertex newVertex = new Vertex(x, y, 0);
         vIndexNew = model.vertexList.size();
         model.vertexList.add(newVertex);
      }

      // Use the value of t to interpolate the color of the new vertex.
      final int cIndexI = ls.cIndexList.get(1 - vOutside);
      final int cIndexO = ls.cIndexList.get(    vOutside);
      float cI[] = model.colorList.get(cIndexI).getRGBColorComponents(null);
      float cO[] = model.colorList.get(cIndexO).getRGBColorComponents(null);
      final float t_ = (float)t;
      final float r = (1 - t_) * cO[0] + t_ * cI[0];
      final float g = (1 - t_) * cO[1] + t_ * cI[1];
      final float b = (1 - t_) * cO[2] + t_ * cI[2];

      // Modify the Model to contain the new Color.
      final Color newColor = new Color(r, g, b);
      final int cIndexNew = model.colorList.size();
      model.colorList.add(newColor);

      if (Clip.debug)
      {
         final String vOut = (0==vOutside) ? "v0" : "v1";
         logMessage(
            String.format("-- Clip off %s at %s", vOut, equation));
         logMessage(
            String.format("-- t = % .25f", t));
         logMessage(
            String.format("-- <x_i, y_i> = <% .24f, % .24f>",  vIx, vIy));
         logMessage(
            String.format("-- <x_o, y_o> = <% .24f, % .24f>",  vOx, vOy));
         logMessage(
            String.format("-- <x_c, y_c> = <% .24f, % .24f>",    x,   y));
         logMessage(
            String.format("-- <r_i, g_i, b_i> = <% .15f,  % .15f,  % .15f>",
                              cI[0], cI[1], cI[2]));
         logMessage(
            String.format("-- <r_o, g_o, b_o> = <% .15f,  % .15f,  % .15f>",
                              cO[0], cO[1], cO[2]));
         logMessage(
            String.format("-- <r_c, g_c, b_c> = <% .15f,  % .15f,  % .15f>",
                               r,   g,   b));
      }

      // Return a new LineSegment using the new Vertex and Color and
      // keeping the old LineSegment's inside Vertex and Color.
      final LineSegment newLS;
      if (1 == vOutside)
      {
         newLS = new LineSegment(vIndex0, vIndexNew,
                                 cIndexI, cIndexNew);
      }
      else
      {
         newLS = new LineSegment(vIndexNew, vIndex1,
                                 cIndexNew, cIndexI);
      }
      return newLS;
   }



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