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

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

/**
   This class holds all the client state information
   and implementations of the client's event handlers.
<p>
   This version allows non-symmetric view volumes.
*/
public abstract class InteractiveAbstractClient_v2_R5 implements
                                                      KeyListener,
                                                      ComponentListener
{
   protected final int WINDOW_SIZE = 700;

   // This is the GUI state data (the Model in MVC).
   protected boolean letterbox = false;
   private double left = -1.0;
   private double right = -left;
   private double bottom = -1.0;
   private double top = -bottom;
   protected double aspectRatio = 1.0;
   private double focalLength = 1.0;
   protected double near = 1.0;
   protected boolean perspective = true;
   protected boolean showCamera = false;
   protected boolean showWindow = false;

   protected boolean displayTranslation = false;
   protected double pushback = -2.0;
   protected double[] deltaX = {0.0};
   protected double[] deltaY = {0.0};
   protected double[] deltaZ = {0.0};

   protected Scene scene = null;
   protected int numberOfInteractiveModels = 1;
   protected boolean interactiveModelsAllVisible = false;
   protected boolean debugWholeScene = true;
   protected int currentModel = 0;
   protected Model savedModel = null; // used to hold a PointCloud model
   protected int pointSize = 0;       // used by the point clouds

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

   protected int pipelineNumber = 1;

   protected JFrame jf = null;
   protected FrameBufferPanel fbp = null;


   /**
      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 Camera camera;
      if (perspective)
      {
         camera = Camera.projPerspective(left, right, bottom, top,
                                         focalLength);
      }
      else
      {
         camera = Camera.projOrtho(left, right, bottom, top);
      }
      scene = scene.changeCamera( 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 )
      {
         final double aspectRatio = (right - left) / (top - bottom);
         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.
      if (pipelineNumber == 1)
      {
         Pipeline.render(scene, fb);
      }
      else
      {
         Pipeline2.render(scene, fb);
      }
      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)
      {
         if (debugWholeScene)
         {
            scene.debug = ! scene.debug;
            Clip.debug = scene.debug;
         }
         else // debug just the current model
         {
            final Position p = scene.getPosition(currentModel);
            p.debug = ! p.debug;
            Clip.debug = p.debug;
         }
      }
      else if ('D' == c)
      {
         Rasterize.debug = ! Rasterize.debug;
      }
      else if ('1' == c)
      {
         pipelineNumber = 1;
         System.out.println("Using pipeline number 1.");
      }
      else if ('2' == c)
      {
         pipelineNumber = 2;
         System.out.println("Using pipeline number 2.");
      }
      else if ('i' == c)
      {
         final Model m = scene.getPosition(currentModel).getModel();
         final int verts = m.vertexList.size();
         int points = 0;
         int lines = 0;
         for (final Primitive p : m.primitiveList)
         {
            if (p instanceof Point)
            {
               ++points;
            }
            else if (p instanceof LineSegment)
            {
               ++lines;
            }
         }
         System.out.printf("The current Model has:\n");
         System.out.printf("  %,d vertices,\n", verts);
         System.out.printf("  %,d Point primitives,\n", points);
         System.out.printf("  %,d Line segments.\n", lines);
      }
      else if ('I' == c)
      {
         System.out.println();
         System.out.println(scene.getPosition(currentModel).getModel());
      }
      else if ('/' == c)
      {
         scene.getPosition(currentModel).visible = interactiveModelsAllVisible;
         currentModel = (currentModel + 1) % numberOfInteractiveModels;
         scene.getPosition(currentModel).visible = true;
         savedModel = null;
         pointSize = 0;
         System.out.println("The current model is "
                          + scene.getPosition(currentModel).getModel().name);
      }
      else if ('?' == c)
      {
         scene.getPosition(currentModel).visible = interactiveModelsAllVisible;
         currentModel = currentModel - 1;
         if (currentModel < 0) currentModel = numberOfInteractiveModels - 1;
         scene.getPosition(currentModel).visible = true;
         savedModel = null;
         pointSize = 0;
         System.out.println("The current model is "
                          + scene.getPosition(currentModel).getModel().name);
      }
      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 ('P' == c)
      {
         if (savedModel != null)
         {
            scene.getPosition(currentModel).setModel(savedModel);
            savedModel = null;
            ++pointSize;
         }
         else
         {
            final Model model = scene.getPosition(currentModel).getModel();
            savedModel = model;
            scene.getPosition(currentModel).setModel(
                                 PointCloud.make(model, pointSize));
         }
      }

      // Asymmetric view volumes.
      else if ('j' == c)
      {
         left -= 0.1;
      }
      else if ('J' == c)
      {
         left += 0.1;
      }
      else if ('o' == c)
      {
         right -= 0.1;
      }
      else if ('O' == c)
      {
         right += 0.1;
      }
      else if ('k' == c)
      {
         bottom -= 0.1;
      }
      else if ('K' == c)
      {
         bottom += 0.1;
      }
      else if ('i' == c)
      {
         top -= 0.1;
      }
      else if ('I' == c)
      {
         top += 0.1;
      }
      else if ('f' == c)
      {
         focalLength -= 0.1;
      }
      else if ('F' == c)
      {
         focalLength += 0.1;
      }

      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 ('M' == c)
      {
         showCamera = ! showCamera;
      }
      else if ('c' == c)
      {
         // Change the solid random color of the cube.
         ModelShading.setRandomColor(
                         scene.getPosition(currentModel).getModel());
      }
      else if ('C' == c)
      {
         // Change each color in the cube to a random color.
         ModelShading.setRandomColors(
                         scene.getPosition(currentModel).getModel());
      }
      else if ('e' == c && e.isAltDown())
      {
         // Change the random color of each vertex of the cube.
         ModelShading.setRandomVertexColors(
                         scene.getPosition(currentModel).getModel());
      }
      else if ('e' == c)
      {
         // Change the solid random color of each edge of the cube.
         ModelShading.setRandomPrimitiveColors(
                         scene.getPosition(currentModel).getModel());
      }
      else if ('E' == c)
      {
         // Change the random color of each end of each edge of the cube.
         ModelShading.setRainbowPrimitiveColors(
                         scene.getPosition(currentModel).getModel());
      }
      else if ('>' == c || '<' == c
             ||'.' == c || ',' == c)
      {
         final Position p = scene.getPosition(currentModel);
         if (p.getModel() instanceof MeshMaker)
         {
            final MeshMaker model = (MeshMaker)p.getModel();
            final int n;
            final int k;
            if ('>' == c)
            {
               n = model.getHorzCount() + 1;
               k = model.getVertCount();
            }
            else if ('<' == c)
            {
               n = model.getHorzCount() - 1;
               k = model.getVertCount();
            }
            else if ('.' == c)
            {
               n = model.getHorzCount();
               k = model.getVertCount() + 1;
            }
            else  // ',' == c
            {
               n = model.getHorzCount();
               k = model.getVertCount() - 1;
            }
            try
            {
               final Model newM = model.remake(n, k);
               // Copy the color of the previous model.
               ModelShading.setColor(newM, p.getModel().colorList.get(0));
               scene.getPosition(currentModel).setModel(newM);
               final MeshMaker m = (MeshMaker)newM;
               final int newN = m.getHorzCount();
               final int newK = m.getVertCount();
               System.out.printf("Mesh: n = %3d and k = %3d.\n", newN, newK);
            }
            catch (IllegalArgumentException ex){}
         }
      }
      else if ('m' == c) // Display translation information.
      {
         displayTranslation = ! displayTranslation;
      }
      else if ('*' == c) // Display window information.
      {
         showWindow = ! showWindow;
      }
      else if ('+' == c)
      {
         takeScreenshot = true;
      }

      doTranslation(e);

      displayTranslation(e);

      displayCamera(e);

      displayWindow(e);

      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)
   {
      //System.out.println( e );
      //System.out.printf("JFrame [w = %d, h = %d]: " +
      //                  "FrameBufferPanel [w = %d, h = %d].\n",
      //                  jf.getWidth(), jf.getHeight(),
      //                  fbp.getWidth(), fbp.getHeight());

      // Get the new size of the FrameBufferPanel.
      final int w = fbp.getWidth();
      final int h = fbp.getHeight();

      //System.out.printf(
      //  "Aspect ratio of the framebuffer = %.2f\n", (double)w/(double)h);

      // 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();
   }


   // A client program can override how translations are performed.
   protected void doTranslation(final KeyEvent e)
   {
      final char c = e.getKeyChar();

      if ('=' == c) // Reset the translation vector.
      {
         deltaX[0] =  0;
         deltaY[0] =  0;
         deltaZ[0] = -2;
      }
      else if ('x' == c) // Translate the current model:
      {
         deltaX[0] += -0.1;  // left
      }
      else if ('X' == c)
      {
         deltaX[0] += +0.1;  // right
      }
      else if ('y' == c)
      {
         deltaY[0] += -0.1;  // down
      }
      else if ('Y' == c)
      {
         deltaY[0] += +0.1;  // up
      }
      else if ('z' == c)
      {
         deltaZ[0] += -0.1;  // back
      }
      else if ('Z' == c)
      {
         deltaZ[0] += +0.1;  // forward
      }

      scene.getPosition(currentModel).translate(deltaX[0],
                                                deltaY[0],
                                                deltaZ[0]);
   }


   // A client program can override the printing of translation information.
   protected void displayTranslation(final KeyEvent e)
   {
      final char c = e.getKeyChar();

      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",
                           deltaX[0],
                           deltaY[0],
                           deltaZ[0]);
      }
   }


   // A client program can override the printing of camera information.
   protected void displayCamera(final KeyEvent e)
   {
      final char c = e.getKeyChar();

      if (showCamera && ('M'==c||'n'==c||'N'==c
                               ||'f'==c||'F'==c
                               ||'j'==c||'J'==c
                               ||'k'==c||'K'==c
                               ||'i'==c||'I'==c
                               ||'o'==c||'O'==c
                               ||('b'==c && NearClip.doNearClipping)
                               ||'p'==c))
      {
         System.out.println( scene.camera );
         final double fovx = (180./Math.PI) * Math.atan( scene.camera.right)
                           + (180./Math.PI) * Math.atan(-scene.camera.left);
         System.out.println("   fovx = " + fovx);
         System.out.println("   focalLength = " + focalLength);
      }
   }


   // A client program can override the printing of window information.
   protected void displayWindow(final KeyEvent e)
   {
      //final char c = e.getKeyChar();

      if (showWindow)
      {
         // Get the size of the JFrame.
         final int wJF = jf.getWidth();
         final int hJF = jf.getHeight();
         // Get the size of the FrameBufferPanel.
         final int wFBP = fbp.getWidth();
         final int hFBP = fbp.getHeight();
         // Get the size of the FrameBuffer.
         final int wFB = fbp.getFrameBuffer().getWidthFB();
         final int hFB = fbp.getFrameBuffer().getHeightFB();
         // Get the size of the Viewport.
         final int wVP = fbp.getFrameBuffer().getViewport().getWidthVP();
         final int hVP = fbp.getFrameBuffer().getViewport().getHeightVP();
         // Get the location of the Viewport in the FrameBuffer.
         final int vp_ul_x = fbp.getFrameBuffer().getViewport().vp_ul_x;
         final int vp_ul_y = fbp.getFrameBuffer().getViewport().vp_ul_y;
         // Get the size of the camera's view rectangle.
         final Camera c = scene.camera;
         final double wVR = c.right - c.left;
         final double hVR = c.top - c.bottom;

         final double rJF  = (double)wJF/(double)hJF;
         final double rFBP = (double)wFBP/(double)hFBP;
         final double rFB  = (double)wFB/(double)hFB;
         final double rVP  = (double)wVP/(double)hVP;
         final double rC   = wVR / hVR;

         System.out.printf(
            "Window information:\n" +
             "            JFrame [w=%4d, h=%4d], aspect ratio = %.2f\n" +
             "  FrameBufferPanel [w=%4d, h=%4d], aspect ratio = %.2f\n" +
             "       FrameBuffer [w=%4d, h=%4d], aspect ratio = %.2f\n" +
             "          Viewport [w=%4d, h=%4d, x=%d, y=%d], aspect ratio = %.2f\n" +
             "            Camera [w=%.2f, h=%.2f], aspect ratio = %.2f\n",
             wJF, hJF, rJF,
             wFBP, hFBP, rFBP,
             wFB, hFB, rFB,
             wVP, hVP, vp_ul_x, vp_ul_y, rVP,
             wVR, hVR, rC);
      }
      showWindow = false;
   }


   // A client program can override the printing of help information.
   protected void print_help_message()
   {
      System.out.println("Use the '/' and '?' keys to cycle forwards and backwards through the models.");
      System.out.println("Use the 'd/D' keys to toggle debugging information on and off for the current model.");
      System.out.println("Use the '1' and '2' keys to switch between pipeline 1 and pipeline 2.");
      System.out.println("Use the '>/<' and shift keys to increase and decrease the mesh divisions in each direction.");
      System.out.println("Use the 'i/I' keys to get information about the current model.");
      System.out.println("Use the 'p' key to toggle between parallel and orthographic projection.");
      System.out.println("Use the x/X, y/Y, z/Z, keys to translate the model along the x, y, z axes.");
      System.out.println("Use the 'm' key to toggle the display of translation information.");
      System.out.println("Use the '=' key to reset the model translation.");
      System.out.println("Use the 'c' key to change the random solid model color.");
      System.out.println("Use the 'C' key to randomly change model's colors.");
      System.out.println("Use the 'e' key to change the random solid edge colors.");
      System.out.println("Use the 'E' key to change the random edge colors.");
      System.out.println("Use the 'Alt-e' key combination to change the random vertex 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 j/J keys to change the view rectangle's left edge.");
      System.out.println("Use the o/O keys to change the view rectangle's right edge.");
      System.out.println("Use the k/K keys to change the view rectangle's bottom edge.");
      System.out.println("Use the i/I keys to change the view rectangle's top edge.");
      System.out.println("Use the f/F keys to change the camera's focal-length (keep AR constant).");
      System.out.println("Use the 'l' key to toggle letterboxing viewport on and off.");
      System.out.println("Use the 'M' key to toggle showing the Camera data.");
      System.out.println("Use the '*' key to show window 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();
   }
}
