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

import renderer.scene.*;
import renderer.scene.util.Assets;
import renderer.scene.util.ModelShading;
import renderer.scene.util.DrawSceneGraph;
import renderer.models_L.*;
import renderer.pipeline.*;
import renderer.framebuffer.*;

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

/**
   Compare with
      http://threejs.org/examples/#webgl_geometries
   or
      https://stemkoski.github.io/Three.js/Shapes.html
   or
      http://www.smartjava.org/ltjs/chapter-02/04-geometries.html
*/
public class Geometries_R5_navigate implements
                                    KeyListener,
                                    ComponentListener
{
   private static final String assets = Assets.getPath();

   // This is the GUI state data (the Model in MVC).
   private double cameraX =  0.0; // ised to simulate camera movement.
   private double cameraY =  3.0;
   private double cameraZ = 10.0;

   private boolean letterbox = false;
   private double aspectRatio = 2.0;
   private double near = 1.0;
   private double fovy = 90.0;
   private boolean perspective = true;
   private boolean showCamera = false;
   private boolean showWindow = false;

   private Scene scene;
   private final Position[][] position = new Position[5][3];

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

   private int pipelineNumber = 1;

   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 Geometries_R5_navigate()
   {
      scene = new Scene("Geometries_R5_navigate");

      // Create a two-dimensional array of Models.
      final Model[][] model = new Model[5][3];

      // row 0 (front row in the image)
      model[0][0] = new TriangularPrism(1.0, 1.0, 10);
      ModelShading.setColor(model[0][0],
                            Color.green.darker().darker());

      model[0][1] = new Cylinder(0.5, 1.0, 30, 30);
      ModelShading.setColor(model[0][1],
                            Color.blue.brighter().brighter());

      model[0][2] = new ObjSimpleModel(new File(
                          assets + "great_rhombicosidodecahedron.obj"));
      ModelShading.setColor(model[0][2], Color.red);

      // row 1
      model[1][0] = new GRSModel(new File(
                          assets + "grs/bronto.grs"));
      ModelShading.setColor(model[1][0], Color.red);

      model[1][1] = new ObjSimpleModel(new File(
                          assets + "horse.obj"));
      ModelShading.setColor(model[1][1], Color.pink.darker());

      model[1][2] = new ConeFrustum(0.5, 1.0, 1.0, 10, 10);
      ModelShading.setColor(model[1][2], Color.orange.darker());

      // row 2
      model[2][0] = new Torus(0.75, 0.25, 30, 30);
      ModelShading.setColor(model[2][0], Color.gray);

      model[2][1] = new Octahedron(6);
      ModelShading.setColor(model[2][1], Color.green);

      model[2][2] = new Box(1.0, 1.0, 1.0);
      ModelShading.setRandomPrimitiveColors(model[2][2]);

      // row 3 (back row in the image)
      model[3][0] = new ParametricCurve(
                t -> 0.3*(Math.sin(t) + 2*Math.sin(2*t)) + 0.1*Math.sin(t/6),
                t -> 0.3*(Math.cos(t) - 2*Math.cos(2*t)) + 0.1*Math.sin(t/6),
                t -> 0.3*(-Math.sin(3*t)),
                0, 6*Math.PI, 120);
      ModelShading.setRandomPrimitiveColors(model[3][0]);

      model[3][1] = new ObjSimpleModel(new File(
                          assets + "small_rhombicosidodecahedron.obj"));
      ModelShading.setColor(model[3][1], Color.magenta);

      model[3][2] = new SurfaceOfRevolution(
                t -> 1.5*(0.5 + 0.15 * Math.sin(10*t+1.0)*Math.sin(5*t+0.5)),
                -0.1, 0.9,
                30, 30);
      ModelShading.setColor(model[3][2], Color.blue);

      // row 4 (last row in first image)
      model[4][0] = new Cone(0.5, 1.0, 30, 30);
      ModelShading.setColor(model[4][0], Color.yellow);

      model[4][1] = new Tetrahedron(12, 12);
      ModelShading.setColor(model[4][1],
                            Color.green.brighter().brighter());

      model[4][2] = new Sphere(1.0, 30, 30);
      ModelShading.setColor(model[4][2],
                            Color.cyan.brighter().brighter());

      // Create x, y and z axes.
      final Model xyzAxes = new Axes3D(6, -6, 6, 0, 7, -7, Color.red);

      // Create a horizontal coordinate plane model.
      final Model xzPlane = new PanelXZ(-6, 6, -7, 7);
      ModelShading.setColor(xzPlane, Color.darkGray);

      // Add the models to positions in the Scene.
      scene.addPosition( new Position(xzPlane, // Draw the grid first.
                                      new Vector(-cameraX, -cameraY, -cameraZ)),
                         new Position(xyzAxes, // Draw the axes on top of the grid.
                                      new Vector(-cameraX, -cameraY, -cameraZ)));

      for (int i = model.length - 1; i >= 0; --i) // From back to front.
      {
         for (int j = 0; j < model[i].length; ++j)
         {
            position[i][j] =
               new Position(
                      model[i][j],
                      "p["+i+"]["+j+"]",
                      new Vector(-cameraX -4 + 4*j,
                                 -cameraY,
                                 -cameraZ + 6 - 3*i));

            scene.addPosition( position[i][j] );
         }
      }

      DrawSceneGraph.drawVertexList = false;
      DrawSceneGraph.draw(scene, "Geometries_R5_navigate_SG");


      // Create a FrameBufferPanel that holds a FrameBuffer.
      final int width  = 1800;
      final int height =  900;
      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 - Geometries Navigate");
      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.
   */
   private void setupViewing()
   {
      // Set up the camera's view volume.
      final Camera camera1;
      if (perspective)
      {
         camera1 = Camera.projPerspective(fovy, aspectRatio);
      }
      else
      {
         camera1 = Camera.projOrtho(fovy, aspectRatio);
      }
      final Camera camera2 = camera1.changeNear(near);
      // Switch cameras.
      scene = scene.changeCamera( camera2 );

      // 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.
      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
   }


   // Implement the KeyListener interface.
   @Override public void keyPressed(KeyEvent e)
   {
      //System.out.println( e );

      final int keyCode = e.getKeyCode();
      // Only handle the four arrow keys.
      if (KeyEvent.VK_UP == keyCode   || KeyEvent.VK_DOWN == keyCode
       || KeyEvent.VK_LEFT == keyCode || KeyEvent.VK_RIGHT == keyCode)
      {
         if ( 0 != (e.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) )
         {
            if (KeyEvent.VK_UP == keyCode)
            {
               cameraY += 0.1;
            }
            else if (KeyEvent.VK_DOWN == keyCode)
            {
               cameraY -= 0.1;
            }
         }
         else // no control key
         {
            if (KeyEvent.VK_UP == keyCode )
            {
               cameraZ -= 0.1;
            }
            else if (KeyEvent.VK_DOWN == keyCode )
            {
               cameraZ += 0.1;
            }
            else if (KeyEvent.VK_LEFT == keyCode )
            {
               cameraX -= 0.1;
            }
            else if (KeyEvent.VK_RIGHT == keyCode )
            {
               cameraX += 0.1;
            }
         }

         // Simulate moving the camera by moving every
         // model in the scene in the direction opposite
         // to what the camera supposedly moves in.
         scene.getPosition(0).translate(-cameraX, -cameraY, -cameraZ);
         scene.getPosition(1).translate(-cameraX, -cameraY, -cameraZ);
         for (int i = position.length - 1; i >= 0; --i) // from back to front
         {
            for (int j = 0; j < position[i].length; ++j)
            {
               position[i][j].translate(-cameraX -4 + 4*j,
                                        -cameraY,
                                        -cameraZ + 6 - 3*i);
            }
         }

         displayCamera(e);

         // Render again.
         setupViewing();
      }
   }

   @Override public void keyReleased(KeyEvent e){}

   @Override public void keyTyped(KeyEvent 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 ('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 ('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 || 'M' == c)
      {
         showCamera = ! showCamera;
      }
      else if ('*' == c) // Display window information.
      {
         showWindow = ! showWindow;
      }
      else if ('+' == c)
      {
         takeScreenshot = true;
      }

      displayCamera(e);
      displayWindow(e);

      // Render again.
      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);

      // Render again.
      setupViewing();
   }


   private void displayCamera(final KeyEvent e)
   {
      final char c = e.getKeyChar();

      if (showCamera && ('m'==c||'M'==c
                       ||'n'==c||'N'==c
                       ||'f'==c||'F'==c
                       ||'r'==c||'R'==c
                       ||('b'==c && NearClip.doNearClipping)
                       ||'p'==c))
      {
         System.out.println( scene.camera );
      }

      final int keyCode = e.getKeyCode();
      if (showCamera && ( KeyEvent.VK_UP == keyCode
                       || KeyEvent.VK_DOWN == keyCode
                       || KeyEvent.VK_LEFT == keyCode
                       || KeyEvent.VK_RIGHT == keyCode) )
      {
         System.out.print("Camera \"Location\": ");
         System.out.println("(cameraX, cameraY, cameraZ) = (" + cameraX + ", " + cameraY + ", " + cameraZ + ")");
      }
   }


   private 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;
   }


   private void print_help_message()
   {
      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 'p' key to toggle between parallel and orthographic projection.");
      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 (keep AR constant).");
      System.out.println("Use the r/R keys to change the camera's aspect ratio (keep fov constant).");
      System.out.println("Use the 'l' key to toggle letterboxing viewport on and off.");
      System.out.println("Use the arrow keys to \"translate\" the camera left/right/forward/backward.");
      System.out.println("Use CTRL arrow keys to \"translate\" the camera up/down.");
      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 '+' 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)
   {
      // We need to call the program's constructor in the
      // Java GUI Event Dispatch Thread, otherwise we get a
      // race condition between the constructor (running in
      // the main() thread) and the very first ComponentEvent
      // (running in the EDT).
      javax.swing.SwingUtilities.invokeLater(
         () -> new Geometries_R5_navigate()
      );
   }
}
