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

import renderer.scene.*;
import renderer.scene.util.ModelShading;
import renderer.models_L.Disk;
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;
import javax.swing.Timer;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

/**
   This abstract class sets up the basic geometry, the GUI,
   and the event handlers.
   <p>
   Subclasses should override the {@code setupViewing()} method
   and use that method to define the camera's view volume and
   the framebuffer's viewport.
   <p>
   This class sets the view volume and the viewport so that the
   standard view volume is distorted onto the whole framebuffer.
*/
@SuppressWarnings("serial")
public abstract class Circle_v0_Abstract extends JFrame
       implements KeyListener, ComponentListener, ActionListener
{
   protected final int SIZE = 512;

   protected boolean showViewData = false;
   protected boolean viewDataChanged = false;
   protected int mode = 1;

   protected Scene scene;
   protected final FrameBufferPanel fbp;

   protected Position modelToRotate = null;
   protected boolean animation = true;
   protected boolean debugFrame = false;
   protected final Timer timer;

   /**
      This constructor instantiates the Scene object
      and initializes it with appropriate geometry.
      Then this constructor instantiates the GUI.
   */
   protected Circle_v0_Abstract(String title)
   {
      super(title);
      setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

      scene = new Scene( Camera.projOrtho() );

      // Create a model of a circle.
      final Model model = new Disk(1.0, 10, 40);
      final Position position = new Position(model);
      modelToRotate = position;
      ModelShading.setRandomColor(model);
      scene.addPosition(position);

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

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

      // Place the FrameBufferPanel in this JFrame.
      this.getContentPane().add(fbp, BorderLayout.CENTER);
      this.pack();
      this.setLocationRelativeTo(null);
      this.setVisible(true);

      // Create a Timer object that will generate ActionEvent
      // objects for the ActionListener handler to respond to.
      final int fps = 30;
      timer = new Timer(1000/fps, this);
      timer.start();
   }


   /**
      Get in one place the code to set up
      the view volume and the viewport.
   */
   protected void setupViewing()
   {
      if (animation)
      {
         timer.start(); // Start the animation.
      }
      else
      {
         timer.stop(); // Stop the animation.
      }

      if (showViewData && viewDataChanged)
      {
         displayViewDtata();
         viewDataChanged = false;
      }

      // Render again.
      final FrameBuffer fb = fbp.getFrameBuffer();
      fb.clearFB();
      fb.vp.clearVP();
      if (debugFrame) // debug one frame of the animation
      {
         scene.debug = true;
         Clip.debug = true;
      }
      Pipeline.render(scene, fb);
      debugFrame = false;
      scene.debug = false;
      Clip.debug = false;
      fbp.repaint();
      System.out.flush(); // Because System.out is buffered by renderer.pipeline.PipelineLogger
   }


   protected void displayViewDtata()
   {
      // 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 rFB  = (double)wFB/(double)hFB;
      final double rVP  = (double)wVP/(double)hVP;
      final double rC   = wVR / hVR;

      System.out.printf(
         "= View information =============================================\n" +
         "  FrameBuffer [w=%4d, h=%4d], aspect ratio = %.2f\n" +
         "  Viewport    [w=%4d, h=%4d, x=%d, y=%d], aspect ratio = %.2f\n" +
         "  view-rect   [w= %.2f, h= %.2f], aspect ratio = %.2f\n",
         wFB, hFB, rFB,
         wVP, hVP, vp_ul_x, vp_ul_y, rVP,
         wVR, hVR, rC);
      System.out.println( c );
   }


   // 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 && e.isAltDown())
      {
         System.out.println();
         System.out.println( scene );
      }
      else if ('d' == c)
      {
         debugFrame = true; // debug one frame of animation
      }
      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 ('v' == c)
      {
         showViewData = ! showViewData;
         viewDataChanged = true;
      }
      else if ('c' == c)
      {
         // Change the solid random color of the disk.
         ModelShading.setRandomColor(scene.getPosition(0).getModel());
      }
      else if ('C' == c)
      {
         // Change each color in the disk to a random color.
         ModelShading.setRandomColors(scene.getPosition(0).getModel());
      }
      else if ('e' == c && e.isAltDown())
      {
         // Change the random color of each vertex of the disk.
         ModelShading.setRandomVertexColors(scene.getPosition(0).getModel());
      }
      else if ('e' == c)
      {
         // Change the solid random color of each edge of the disk.
         ModelShading.setRandomPrimitiveColors(scene.getPosition(0).getModel());
      }
      else if ('E' == c)
      {
         // Change the random color of each end of each edge of the disk.
         ModelShading.setRainbowPrimitiveColors(scene.getPosition(0).getModel());
      }
      else if ('s' == c || 'S' == c)
      {
         animation = ! animation;
      }
      else if ('1' == c)
      {
         mode = 1;
         viewDataChanged = true;
      }
      else if ('2' == c)
      {
         mode = 2;
         viewDataChanged = true;
      }
      else if ('3' == c)
      {
         mode = 3;
         viewDataChanged = true;
      }
      else if ('4' == c)
      {
         mode = 4;
         viewDataChanged = true;
      }
      else if ('5' == c)
      {
         mode = 5;
         viewDataChanged = true;
      }
      else if ('6' == c)
      {
         mode = 6;
         viewDataChanged = true;
      }
      else if ('7' == c)
      {
         mode = 7;
         viewDataChanged = true;
      }
      else if ('8' == c)
      {
         mode = 8;
         viewDataChanged = true;
      }
      else if ('9' == c)
      {
         mode = 9;
         viewDataChanged = true;
      }

      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 );

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

      viewDataChanged = true;

      if (0 == hFB) return; // there is no window to draw

      // 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(wFB, hFB, bg1);
      fb.vp.setBackgroundColorVP(bg2);
      fbp.setFrameBuffer(fb);

      setupViewing();
   }


   // Implement the ActionListener interface (for the timer).
   @Override public void actionPerformed(ActionEvent e)
   {
      rotateModel(modelToRotate, 10); // 10 degrees
      setupViewing();
   }


   protected void rotateModel(final Position p,
                              final double angle) // angle in degrees
   {
      final Model m = p.getModel();

      final double angleRad = angle * Math.PI / 180.0; // convert to radians

      for (int i = 0; i < m.vertexList.size(); ++i)
      {
         final Vertex v = m.vertexList.get(i);
         m.vertexList.set(i, new Vertex(
                               v.x * Math.cos(angleRad) - v.y * Math.sin(angleRad),
                               v.x * Math.sin(angleRad) + v.y * Math.cos(angleRad),
                               v.z));
      }
   }


   protected void print_help_message()
   {
      System.out.println("Use the 'd' key to debug one frame of animation.");
      System.out.println("Use the 'Alt-d' key combination to print the Scene data structure.");
      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 'v' key to toggle showing the view data.");
      System.out.println("Use the 's' key to stop/start the rotation.");
      System.out.println("Use the '1' through '9' keys to choose a letterbox/crop/window mode.");
      System.out.println("Use the 'h' key to redisplay this help message.");
      System.out.println();
      System.out.println("The nine letterbox/crop/window modes are:");
      System.out.println("  1 - top left-hand corner");
      System.out.println("  2 - top center");
      System.out.println("  3 - top right-hand corner");
      System.out.println("  4 - right center");
      System.out.println("  5 - botton right-hand corner");
      System.out.println("  6 - bottom center");
      System.out.println("  7 - botton left-hand corner");
      System.out.println("  8 - left center");
      System.out.println("  9 - center");
      System.out.flush();
   }
}
