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

import renderer.scene.*;
import renderer.scene.primitives.*;
import renderer.scene.util.PointCloud;
import renderer.scene.util.MeshMaker;
import renderer.scene.util.ModelShading;
import renderer.models_L.Sphere;
import renderer.pipeline.*;
import renderer.framebuffer.*;

import java.awt.Color;
import javax.swing.JFrame;
import java.awt.BorderLayout;
import java.awt.event.KeyListener;
import java.awt.event.KeyEvent;
import java.awt.event.ComponentListener;
import java.awt.event.ComponentEvent;

/**
   Put the camera at the origin of world coordinates looking
   down the negative z-axis. This way, world coordinates and
   view coordinates coincide.
<p>
   Put a sphere of radius 1/4 at center (0, 0, -5/4) and put
   another sphere of radius 1/4 at center (0, 0, -7/4) so
   the second sphere is right behind and just touching the
   first sphere. Initialize the near plane with near = 1, so
   the near plane is just touching the front of the first
   sphere (recall that the near plane is z = -near in view
   coordinates).
<p>
   The N-key will move the near plane towards the first
   sphere (and the Z-key will move the first sphere towards
   the near plane). Each tap of the N-key moves the near
   plane by 0.01, so it takes 50 taps to move the near plane
   through the whole first sphere. Notice that at the 25'th
   tap of the N-key, the first sphere starts to shrink in
   radius because at that point the near plane has passed
   through more than half of the sphere.
<p>
   After 50 taps of the N-key, only the second sphere remains
   and the near plane is just touching it, so 50 more taps of
   the N-key will make the near plane pass through it also.
<p>
   At any time you can switch from perspective projection to
   orthographic projection and it should not change how much
   of the sphere has been clipped off.
*/
public class TestNearClippingPlane implements
                                   KeyListener,
                                   ComponentListener
{
   private final int WINDOW_SIZE = 700;

   // This is the GUI state data (the Model in MVC).
   private boolean letterbox = false;
   private double aspectRatio = 1.0;
   private double near = 1.0;
   private double fovy = 90.0;
   private boolean perspective = true;
   private boolean showCamera = false;

   private boolean displayTranslation = false;
   private double xTranslation = 0.0;
   private double yTranslation = 0.0;
   private double zTranslation = 0.0;

   private boolean takeScreenshot = false;
   private int screenshotNumber = 0;

   private final Scene scene;

   private final JFrame jf;
   private final FrameBufferPanel fbp;

   /**
      This constructor instantiates the Scene object
      and initializes it with appropriate geometry.
      Then this constructor instantiates the GUI.
   */
   public TestNearClippingPlane()
   {
      scene = new Scene("TestNearClippingPlane");

      // Create two sphere Models with radius 1/4.
      final Model m1 = new Sphere(0.25, 35, 35);
      final Model m2 = new Sphere(0.25, 50, 50);

      // Put the sphere m1 at the edge of the initial near
      // plane (z = -1 in view coordinates).
      xTranslation =  0.0;
      yTranslation =  0.0;
      zTranslation = -1.25;
      final Vector v1 = new Vector(xTranslation,
                                   yTranslation,
                                   zTranslation);

      // Put sphere m2 right behind sphere m1.
      final Vector v2 = new Vector(xTranslation,
                                   yTranslation,
                                   zTranslation - 0.5);

      final Position p1 = new Position(m1, v1);
      final Position p2 = new Position(m2, v2);

      // Make the sphere that's in front blue
      // and the sphere that's in back red.
      ModelShading.setColor(m1, Color.blue);
      ModelShading.setColor(m2, Color.red);

      // Add the position that is in back first in the positionList and
      // put the position that is in front second in the positionList
      // (to take advantage of the "painter's algorithm").
      scene.addPosition(p2);
      scene.addPosition(p1);

      showCamera = true;


      // Create a FrameBufferPanel that holds a FrameBuffer.
      final int width  = WINDOW_SIZE;
      final int height = WINDOW_SIZE;
      fbp = new FrameBufferPanel(width, height, Color.darkGray);
      fbp.getFrameBuffer().getViewport().setBackgroundColorVP(Color.black);

      // Register this object as the event listener for FrameBufferPanel events.
      fbp.addKeyListener(this);
      fbp.addComponentListener(this);

      // Create a JFrame that will hold the FrameBufferPanel.
      jf = new JFrame("Renderer 5 - Test Near Clipping Plane");
      jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      jf.getContentPane().add(fbp, BorderLayout.CENTER);
      jf.pack();
      jf.setLocationRelativeTo(null);
      jf.setVisible(true);

      print_help_message();
   }


   /**
      This method is the View part of MVC.
   <p>
      Get in one place the code to set up
      the viewport and view volume.
   */
   protected void setupViewing()
   {
      // Set up the camera's view volume.
      final Scene scene2;
      if (perspective)
      {
         scene2 = scene.changeCamera(
                          Camera.projPerspective(fovy, aspectRatio));
      }
      else
      {
         scene2 = scene.changeCamera(
                          Camera.projOrtho(fovy, aspectRatio));
      }
      final Scene scene3 = scene2.changeCamera(scene2.camera.changeNear(near));

      // Get the size of the FrameBuffer.
      final FrameBuffer fb = fbp.getFrameBuffer();
      final int w = fb.getWidthFB();
      final int h = fb.getHeightFB();
      // Create a viewport with the correct aspect ratio.
      if ( letterbox )
      {
         if ( aspectRatio <= w/(double)h )
         {
            final int width = (int)(h * aspectRatio);
            final int xOffset = (w - width) / 2;
            fb.setViewport(xOffset, 0, width, h);
         }
         else
         {
            final int height = (int)(w / aspectRatio);
            final int yOffset = (h - height) / 2;
            fb.setViewport(0, yOffset, w, height);
         }
         fb.clearFB();
         fb.vp.clearVP();
      }
      else // The viewport is the whole framebuffer.
      {
         fb.setViewport();
         fb.vp.clearVP();
      }

      // Render again.
      Pipeline.render(scene3, fb.vp);
      if (takeScreenshot)
      {
         fb.dumpFB2File(String.format("Screenshot%03d.png", screenshotNumber),
                        "png");
         ++screenshotNumber;
         takeScreenshot = false;
      }
      fbp.repaint();
      System.out.flush(); // Because System.out is buffered by renderer.pipeline.PipelineLogger
   }//setupViewing()


   // Implement the KeyListener interface.
   @Override public void keyPressed(KeyEvent e){}
   @Override public void keyReleased(KeyEvent e){}
   @Override public void keyTyped(KeyEvent e)
   {
      //System.out.println( e );
      final char c = e.getKeyChar();
      if ('h' == c)
      {
         print_help_message();
         return;
      }
      else if ('d' == c)
      {
         scene.debug = ! scene.debug;
         Clip.debug = scene.debug;
      }
      else if ('a' == c)
      {
         Rasterize.doAntiAliasing = ! Rasterize.doAntiAliasing;
         System.out.print("Anti-aliasing is turned ");
         System.out.println(Rasterize.doAntiAliasing ? "On" : "Off");
      }
      else if ('g' == c)
      {
         Rasterize.doGamma = ! Rasterize.doGamma;
         System.out.print("Gamma correction is turned ");
         System.out.println(Rasterize.doGamma ? "On" : "Off");
      }
      else if ('p' == c)
      {
         perspective = ! perspective;
         final String p = perspective ? "perspective" : "orthographic";
         System.out.println("Using " + p + " projection");
      }
      else if ('l' == c)
      {
         letterbox = ! letterbox;
         System.out.print("Letter boxing is turned ");
         System.out.println(letterbox ? "On" : "Off");
      }
      else if ('n' == c)
      {
         // Move the near plane closer to the camera.
         near -= 0.01;
      }
      else if ('N' == c)
      {
         // Move the near plane away from the camera.
         near += 0.01;
      }
      else if ('b' == c)
      {
         NearClip.doNearClipping = ! NearClip.doNearClipping;
         System.out.print("Near-plane clipping is turned ");
         System.out.println(NearClip.doNearClipping ? "On" : "Off");
      }
      else if ('r' == c || 'R' == c)
      {
         // Change the aspect ratio of the camera's view rectangle.
         if ('r' == c)
         {
            aspectRatio -= 0.01;
         }
         else
         {
            aspectRatio += 0.01;
         }
      }
      else if ('f' == c)
      {
         fovy -= 0.5;  // change by 1/2 a degree
      }
      else if ('F' == c)
      {
         fovy += 0.5;  // change by 1/2 a degree
      }
      else if ('M' == c)
      {
         showCamera = ! showCamera;
      }
      else if ('c' == c)
      {
         // Change the solid random color of each model.
         for (final Position p : scene.positionList)
         {
            ModelShading.setRandomColor(p.getModel());
         }
      }
      else if ('m' == c) //display translation information
      {
         displayTranslation = ! displayTranslation;
      }
      else if ('=' == c) // Reset the translation vector.
      {
         xTranslation =  0;
         yTranslation =  0;
         zTranslation = -2;
      }
      else if ('z' == c)
      {
         zTranslation += -0.1;  // back
      }
      else if ('Z' == c)
      {
         zTranslation += +0.1;  // forward
      }
      else if ('m' == c) //display translation information
      {
         displayTranslation = ! displayTranslation;
      }
      else if ('+' == c)
      {
         takeScreenshot = true;
      }

      // Translate the sphere that is front, which
      // is in the second position.
      scene.setPosition(1,
         scene.getPosition(1).translate(xTranslation,
                                        yTranslation,
                                        zTranslation));

      if (displayTranslation && ('m'==c||'='==c
                               ||'x'==c||'y'==c||'z'==c
                               ||'X'==c||'Y'==c||'Z'==c))
      {
         System.out.printf("deltaX = %.2f, " +
                           "deltaY = %.2f, " +
                           "deltaZ = %.2f\n",
                           xTranslation,
                           yTranslation,
                           zTranslation);
      }

      if (showCamera && ('M'==c||'n'==c||'N'==c
                               ||'f'==c||'F'==c
                               ||'r'==c||'R'==c
                               ||'p'==c))
      {
         System.out.println( scene.camera );
      }
      if (showCamera && ('M'==c||'r'==c||'R'==c))
      {
         final int w = fbp.getWidth();
         final int h = fbp.getHeight();
         System.out.printf(
           "Aspect ratio of the framebuffer = %.2f\n", (double)w/(double)h);
      }

      setupViewing();
   }//keyTyped()


   // Implement the ComponentListener interface.
   @Override public void componentMoved(ComponentEvent e){}
   @Override public void componentHidden(ComponentEvent e){}
   @Override public void componentShown(ComponentEvent e){}
   @Override public void componentResized(ComponentEvent e)
   {
      // Get the new size of the FrameBufferPanel.
      final int w = fbp.getWidth();
      final int h = fbp.getHeight();

      // Create a new FrameBuffer that fits the FrameBufferPanel.
      final Color bg1 = fbp.getFrameBuffer().getBackgroundColorFB();
      final Color bg2 = fbp.getFrameBuffer().getViewport()
                                            .getBackgroundColorVP();
      final FrameBuffer fb = new FrameBuffer(w, h, bg1);
      fb.vp.setBackgroundColorVP(bg2);
      fbp.setFrameBuffer(fb);

      setupViewing();
   }


   private void print_help_message()
   {
      System.out.println("Use the 'd' key to toggle debugging information on and off.");
      System.out.println("Use the 'p' key to toggle between parallel and orthographic projection.");
      System.out.println("Use the z/Z, keys to translate the first sphere along the z axis.");
      System.out.println("Use the 'm' key to toggle showing the translation vector.");
      System.out.println("Use the '=' key to reset the translation vector.");
      System.out.println("Use the 'c' key to change the solid model colors.");
      System.out.println("Use the 'a' key to toggle anti-aliasing on and off.");
      System.out.println("Use the 'g' key to toggle gamma correction on and off.");
      System.out.println("Use the 'b' key to toggle near plane clipping on and off.");
      System.out.println("Use the n/N keys to move the camera's near plane.");
      System.out.println("Use the f/F keys to change the camera's field-of-view.");
      System.out.println("Use the r/R keys to change the aspect ratio of the camera's view rectangle.");
      System.out.println("Use the 'l' key to toggle letterboxing on and off.");
      System.out.println("Use the 'M' key to toggle showing the Camera data.");
      System.out.println("Use the 'P' key to convert the current model to a point cloud.");
      System.out.println("Use the '+' key to save a \"screenshot\" of the framebuffer.");
      System.out.println("Use the 'h' key to redisplay this help message.");
      System.out.flush();
   }


   /**
      Create an instance of this class which has
      the affect of creating the GUI application.
   */
   public static void main(String[] args)
   {
      javax.swing.SwingUtilities.invokeLater(
         () -> new TestNearClippingPlane()
      );
   }
}
