/// LSU EE 4702-1 (Fall 2021), GPU Programming
//
 /// Homework 1 -- SOLUTION
 //
 //  Based on demo-05.

/// What Code Does

// Shows a sphere and a triangle.


///  Keyboard Commands
 //
 /// Object (Eye, Light, Ball) Location or Push
 //   Arrows (←,→,↑,↓) Page Up, Page Down
 //        Move object or push ball, depending on mode.
 //        Shift + KEY: motion is 5x faster.
 //        Ctrl + KEY : motion is 5x slower.
 //   'e': Move eye.
 //   'l': Move light.
 //   'b': Move sphere.
 //
 /// Eye Direction and GUI Options
 //
 //   'Home', 'End', 'Delete', 'Insert'
 //         Turn the eye direction. Home should rotate eye direction
 //         up, End should rotate eye down, Delete should rotate eye
 //         left, Insert should rotate eye right. The eye direction
 //         vector is displayed in the upper left.
 //  'C-='  (Ctrl =) Increase green text size.
 //  'C--'  (Ctrl -) Decrease green text size.
 //  'F12'  Write screenshot to PNG file.

 /// Simulation Options
 //  (Also see variables below.)
 //
 //  'F12'  Write screenshot to file.

 /// Variables
 //   Selected program variables can be modified using the keyboard.
 //   Use "Tab" to cycle through the variable to be modified, the
 //   name of the variable is displayed next to "VAR" on the bottom
 //   line of green text.

 //  'Tab' Cycle to next variable.
 //  '`'   Cycle to previous variable.
 //  '+'   Increase variable value.
 //  '-'   Decrease variable value.
 //
 //  VAR Light Intensity - The light intensity.


// Include files provided for this course.
//
#define MAIN_INCLUDE
#include <vhelper.h>

#include <vstroke.h>
#include <gp/coord.h>
#include <gp/pstring.h>
#include <gp/misc.h>
#include <gp/colors.h>

#include <vutil-texture.h>
#include <vutil-pipeline.h>
#include "shapes.h"


class World {
public:
  World(pVulkan_Helper &vh)
    :vh(vh),ff_state(vh.qs),frame_timer(vh.frame_timer),shapes(ff_state),
     transform(vh.qs){}
  void setup_and_run();
  void render(vk::CommandBuffer& cb);
  void cb_keyboard();

  // Class providing utilities, such as showing text.
  //
  pVulkan_Helper& vh;
  VFixed_Function_State_Manager ff_state;

  // Class for showing frame timing.
  //
  pFrame_Timer& frame_timer;

  Shapes shapes;

  // Class for easy keyboard control of variables.
  //
  pVariable_Control variable_control;

  bool opt_show_lines; // Set normal to triangle normal.

  pCoor light_location;
  float opt_light_intensity;
  enum { MI_Eye, MI_Light, MI_Ball, MI_Ball_V, MI_COUNT } opt_move_item;

  bool opt_tryout1, opt_tryout2, opt_tryout3;
  float opt_tryoutf;

  int slices; // Level of detail for sphere.
  pCoor sphere_location;
  float sphere_radius;

  pCoor eye_location;
  pVect eye_direction;

  bool global_transform_stale;

  VTransform transform;
  VBufferV<Uni_Lighting> uni_light, *puni_light;
  VPipeline pipe_lonely;
  VPipeline pipe_sphere;
  VVertex_Buffer_Set bset_lonely, bset_sphere;

  /// For Homework 1 (2021)
  //
  bool opt_recompute;
  int opt_n_objects;
  int slices_seen;
  //
  // Homework 1: Feel free to declare new members.

};

void
World::setup_and_run()
{
  // Set up Vulkan.
  //
  vh.init();

  // Set per-frame callback.
  //
  vh.display_cb_set([&](){});
  //
  // In this case it is being set to an empty lambda function.

  // Set render command recording callback.
  //
  vh.cbs_cmd_record.push_back( [&](vk::CommandBuffer& cb){ render(cb); });

  eye_location = pCoor(1,0.5,3);
  eye_location = pCoor(1,.5,10.2);
  eye_direction = pVect(0,0,-1);

  opt_light_intensity = 4.3;
  light_location = pCoor(6.2,0.0,3.7);

  opt_tryout1 = opt_tryout2 = opt_tryout3 = false;
  opt_tryoutf = 0.8;
  variable_control.insert(opt_tryoutf,"tryoutf");

  sphere_location = pCoor(-1,0,-0.1);
  sphere_radius = 2;

  slices = 20;
  variable_control.insert(slices,"Slices in Sphere");

  variable_control.insert(opt_light_intensity,"Light Intensity");
  variable_control.insert(sphere_radius,"sphere_radius");

  opt_move_item = MI_Eye;

  opt_show_lines = false;

  uni_light.init(vh.dev_phys,vh.dev,vk::BufferUsageFlagBits::eUniformBuffer);
  pColor black(0,0,0,0);
  uni_light->cgl_LightModel.ambient = pColor(.4,.4,.4,1);
  uni_light->cgl_LightSource[0].position = light_location;
  uni_light->cgl_LightSource[0].diffuse = pColor(1,1,1,1);
  uni_light->cgl_LightSource[0].ambient = black;
  uni_light->cgl_LightSource[0].specular = black;
  uni_light->cgl_LightSource[0].constantAttenuation = .5;
  uni_light->cgl_LightSource[0].linearAttenuation = 1.0;
  uni_light->cgl_LightSource[0].quadraticAttenuation = 0;

  puni_light = &uni_light;

  opt_recompute = true;
  opt_n_objects = 3;
  slices_seen = -1;

  variable_control.insert(opt_n_objects,"opt_n_objects",1,1);

  // Start the graphics. The function below does not return until the
  // user exits by closing the window.
  //
  vh.message_loop_spin();
  //
  // At this point the user exited and so it's time to
  // clean up.

  uni_light.destroy();
  pipe_lonely.destroy();
  pipe_sphere.destroy();
  bset_lonely.destroy();
  bset_sphere.destroy();
  shapes.destroy();

  vh.finish();
}

void
World::render(vk::CommandBuffer& cb)
{
  // This routine called whenever window needs to be updated.

  // Get any waiting keyboard commands.
  //
  cb_keyboard();

  /// Frame Buffer Informational Messages
  //
  //  Print messages using utility functions provided for this course.
  //
  vh.fbprintf("%s\n",frame_timer.frame_rate_text_get());

  const double time_now = time_wall_fp();
  const bool blink_visible = int64_t(time_now*3) & 1;
# define BLINK(txt,pad) ( blink_visible ? txt : pad )

  vh.fbprintf
    ("Compiled: %s\n",
#ifdef __OPTIMIZE__
     "WITH OPTIMIZATION"
#else
     BLINK("WITHOUT OPTIMIZATION","")
#endif
     );

  vh.fbprintf
    ("Eye location: [%5.1f, %5.1f, %5.1f]  "
     "Eye direction: [%+.2f, %+.2f, %+.2f]\n",
     eye_location.x, eye_location.y, eye_location.z,
     eye_direction.x, eye_direction.y, eye_direction.z);

  vh.fbprintf
    ("Light location: [%5.1f, %5.1f, %5.1f]  Render Using: ('n') %s\n",
     light_location.x, light_location.y, light_location.z,
     opt_show_lines ? "LINES" : "TRIANGLES" );

  vh.fbprintf
    ("Recompute: %s ('r')  "
     "Tryout 1: %s  ('y')  Tryout 2: %s  ('Y')  Tryout 3: %s ('Z')\n",
     opt_recompute ? " ON" : "OFF",
     opt_tryout1 ? BLINK("ON ","   ") : "OFF",
     opt_tryout2 ? BLINK("ON ","   ") : "OFF",
     opt_tryout3 ? BLINK("ON ","   ") : "OFF"
     );

  pVariable_Control_Elt* const cvar = variable_control.current;
  vh.fbprintf("VAR %s = %.5f  (TAB or '`' to change, +/- to adjust)\n",
                      cvar->name,cvar->get_val());


  // -------------------------------------------------------------------------
  ///
  /// Specification of Transformation Matrices
  ///

  /// Set Transformation Matrix for:  Global Space -> Eye Space
  //
  transform.eye_from_global_set
    ( pMatrix_Rotation(eye_direction,pVect(0,0,-1))
      * pMatrix_Translate(-eye_location) );
  //
  // This matrix applies to all rendering pipelines.

  /// Set Transformation Matrix for:  Eye Space -> Clip Space
  //
  const int win_width = vh.get_width();
  const int win_height = vh.get_height();
  const float aspect = float(win_width) / win_height;
  //
  // Frustum: left, right, bottom, top, near, far
  transform.clip_from_eye_set
    ( pMatrix_Frustum(-.8,.8,-.8/aspect,.8/aspect,1,5000) );
  //
  // This matrix also applies to all rendering pipelines.

  /// Set Location and Intensity of Light.
  //
  uni_light->cgl_LightSource[0].diffuse =
    { opt_light_intensity, opt_light_intensity, opt_light_intensity, 1 };
  uni_light->cgl_LightSource[0].position =
    transform.eye_from_global * light_location;
  //
  // Note that the light location is being specified in eye space.
  //
  // Send updated light information to device.
  //
  uni_light.to_dev();

  //
  // -------------------------------------------------------------------------


  ///
  /// Paint Single Triangle.
  ///

  if ( !pipe_lonely )
    pipe_lonely
      .init( vh.qs )
      .use_uni_light( uni_light )
      .color_uniform_back_set( color_red )
      .lighting_on()
      .topology_set( vk::PrimitiveTopology::eTriangleList )
      .create();

  bset_lonely.reset(pipe_lonely);
  transform.use_for(pipe_lonely);

  /// Specify normal for triangle.
  //
  // Use cross product function (in coord.h) to find normal.
  //

  // Object-Space Coordinates
  //
  pCoor p0 = { 0, 0,  0 };
  pCoor p1 = { 9, 6, -4 };
  pCoor p2 = { 0, 5, -3 };

  vector<pCoor> tri_vtx = { p0, p1, p2 };

  pNorm tri_norm = cross( p0, p1, p2 );
  pColor color_tri( .470, .553, .965 ); // Red, Green, Blue

  bset_lonely << color_green << tri_norm << p0;
  bset_lonely << color_lsu_spirit_gold << tri_norm << p1;
  bset_lonely << color_lsu_spirit_purple << tri_norm << p2;


  /// Homework 1, Problem 1: Draw additional triangles closer to the light.
  //  The number of triangles to draw is in opt_n_objects.

  // Problem 1 Hint: 
  //   Compute v0l, a vector from triangle vertex 0 to light.
  //   Use v0l (and other information) to find the coordinate
  //   of one of the vertex of each additional triangle.
  //
  pVect v0l(p0,light_location);

  /// SOLUTION -- Problem 1
  //
  for ( int i=0; i<opt_n_objects; i++ )
    {
      vector<pCoor> nvtx;
      for ( auto& p: tri_vtx )
        {
          pVect vpl(p,light_location);
          pCoor cpl = p + (i+1.0)/(opt_n_objects+1)* vpl;
          bset_lonely << color_light_sea_green << cpl;
          nvtx.push_back(cpl);
        }
      pNorm n = cross(nvtx[0],nvtx[1],nvtx[2]);
      if ( opt_tryout1 )
        bset_lonely << n << n << n;
      else
        bset_lonely << tri_norm << tri_norm << tri_norm;
    }


  bset_lonely.to_dev();
  pipe_lonely.record_draw(cb, bset_lonely);


  ///
  /// Paint a Sphere
  ///

  auto prim_want = opt_show_lines
    ? vk::PrimitiveTopology::eLineStrip
    : vk::PrimitiveTopology::eTriangleList;

  if ( pipe_sphere.p_in_asm_ci.topology != prim_want )
    pipe_sphere.destroy();

  if ( !pipe_sphere )
    pipe_sphere
      .init( vh.qs )
      .use_uni_light( uni_light )
      .lighting_on()
      .topology_set( prim_want )
      .create();

  // Construct color objects using hex RGB codes. See coord.h and colors.h.
  //
  const pColor lsu_spirit_purple(0x580da6);
  const pColor lsu_spirit_gold(0xf9b237);

  pColor color = lsu_spirit_gold;

  const float delta_eta = M_PI / slices;

  /// SOLUTION -- Problem 3
  //
  const bool sphere_stale = slices != slices_seen || opt_recompute;

  if ( sphere_stale )
    {

  bset_sphere.reset(pipe_sphere);

  // Outer (eta) Loop: Iterate over longitude (north-to-south).
  // Inner (theta) Loop: Iterate over latitude (east-to-west)
  //
  for ( int slice = 0; slice < slices - 1; slice++ )
    {
      const float eta0 = slice * delta_eta;
      const float eta1 = eta0 + delta_eta;
      const float y0 = cosf(eta0),        y1 = cosf(eta1);
      const float slice_r0 = sinf(eta0),  slice_r1 = sinf(eta1);
      const float delta_theta = delta_eta * slice_r1;

      for ( float theta0 = 0; theta0 < 2 * M_PI; theta0 += delta_theta )
        {
          const float theta1 = theta0 + delta_theta;

          /// Triangle 1

          // Vertex 1
          bset_sphere
            << color
            << pNorm( slice_r1 * cosf(theta0), y1, slice_r1 * sinf(theta0) )
            << pCoor( slice_r1 * cosf(theta0), y1, slice_r1 * sinf(theta0) )

            // Vertex 2
            << color
            << pNorm( slice_r0 * cosf(theta0), y0, slice_r0 * sinf(theta0) )
            << pCoor( slice_r0 * cosf(theta0), y0, slice_r0 * sinf(theta0) )

            // Vertex 3      
            << color
            << pNorm( slice_r1 * cosf(theta1), y1, slice_r1 * sinf(theta1) )
            << pCoor( slice_r1 * cosf(theta1), y1, slice_r1 * sinf(theta1) )

            /// Triangle 2

            // Vertex 3      
            << color
            << pNorm( slice_r1 * cosf(theta1), y1, slice_r1 * sinf(theta1) )
            << pCoor( slice_r1 * cosf(theta1), y1, slice_r1 * sinf(theta1) )

            // Vertex 2
            << color
            << pNorm( slice_r0 * cosf(theta0), y0, slice_r0 * sinf(theta0) )
            << pCoor( slice_r0 * cosf(theta0), y0, slice_r0 * sinf(theta0) )

            // Vertex 4
            << color
            << pNorm( slice_r0 * cosf(theta1), y0, slice_r0 * sinf(theta1) )
            << pCoor( slice_r0 * cosf(theta1), y0, slice_r0 * sinf(theta1) );
        }
    }

  slices_seen = slices;
  bset_sphere.to_dev();

  }


  // In sphere's coordinate space the sphere center is at the origin
  // and its radius is one. Therefore we need to specify a local to
  // global (global from local) transformation matrix so that sphere
  // is at the value of coordinate sphere_location and its radius is
  // sphere_radius.
  //
  pMatrix glo_from_loc =
    pMatrix_Translate( sphere_location ) * pMatrix_Scale(sphere_radius);
  transform.global_from_local_set_for( glo_from_loc, pipe_sphere );
  //
  // This makes a copy of the global_from_local transformation matrix
  // each time it is called. Because it is a copy it does not affect
  // other pipeline draw operations.

  pipe_sphere.record_draw(cb, bset_sphere);

  /// Homework 1, Problem 2
  //
  //  The code below is not a solution to problem 2, but it might help
  //  you get started.

  if ( opt_tryout3 )
    {
      pMatrix_Translate move_up( 0, 2*sphere_radius*opt_tryoutf,0 );
      // One of the values of opt_tryout1 is correct. Which is it?
      pMatrix glo_from_loc_2 =
        opt_tryout1 ? move_up * glo_from_loc : glo_from_loc * move_up;
      transform.global_from_local_set_for( glo_from_loc_2, pipe_sphere );
      pipe_sphere.record_draw(cb, bset_sphere);
    }

  /// SOLUTION -- Problem 2
  //
  pNorm stol(sphere_location,light_location);
  float s0_dist = 0;
  float radius_last = sphere_radius;

  for ( int i=0; i<opt_n_objects; i++ )
    {
      float radius_here = radius_last * 0.5;
      float dist_to_ctr = s0_dist + radius_last + radius_here;
      pCoor sloc2 = sphere_location + dist_to_ctr * stol;

      transform.global_from_local_set_for
        ( pMatrix_Translate( sloc2 ) * pMatrix_Scale(radius_here),
          pipe_sphere );

      pipe_sphere.record_draw(cb, bset_sphere);

      s0_dist = dist_to_ctr;
      radius_last = radius_here;

    }

  // Render Marker for Light Source
  //
  shapes.record_tetrahedron(cb,transform,light_location,0.2);

}



void
World::cb_keyboard()
{
  const int key = vh.keyboard_key_get();
  if ( !key ) return;
  pVect adjustment(0,0,0);
  pVect user_rot_axis(0,0,0);
  const bool kb_mod_s = vh.keyboard_shift;
  const bool kb_mod_c = vh.keyboard_control;
  const float move_amt = kb_mod_s ? 2.0 : kb_mod_c ? 0.08 : 0.4;


  switch ( key ) {
  case FB_KEY_LEFT: adjustment.x = -move_amt; break;
  case FB_KEY_RIGHT: adjustment.x = move_amt; break;
  case FB_KEY_PAGE_UP: adjustment.y = move_amt; break;
  case FB_KEY_PAGE_DOWN: adjustment.y = -move_amt; break;
  case FB_KEY_DOWN: adjustment.z = move_amt; break;
  case FB_KEY_UP: adjustment.z = -move_amt; break;
  case FB_KEY_DELETE: user_rot_axis.y = 1; break;
  case FB_KEY_INSERT: user_rot_axis.y =  -1; break;
  case FB_KEY_HOME:
    if ( opt_move_item == MI_Light ) adjustment.x = 1; else user_rot_axis.x = 1;
    break;
  case FB_KEY_END: user_rot_axis.x = -1; break;
  case 'b': case 'B': opt_move_item = MI_Ball; break;
  case 'e': case 'E': opt_move_item = MI_Eye; break;
  case 'l': case 'L': opt_move_item = MI_Light; break;
  case 'n': opt_show_lines = !opt_show_lines; break;

  case 'r': case 'R': opt_recompute = !opt_recompute; break;

  case 'y': opt_tryout1 = !opt_tryout1; break;
  case 'Y': opt_tryout2 = !opt_tryout2; break;
  case 'Z': opt_tryout3 = !opt_tryout3; break;

  case FB_KEY_TAB:
    if ( !kb_mod_s ) { variable_control.switch_var_right(); break; }
  case 96: variable_control.switch_var_left(); break;
  case '-':case '_': variable_control.adjust_lower(); break;
  case '+':case '=': variable_control.adjust_higher(); break;
  default: printf("Unknown key, %d\n",key); break;
  }

  // Update eye_direction based on keyboard command.
  //
  if ( user_rot_axis.x || user_rot_axis.y )
    {
      pMatrix_Rotation rotall(eye_direction,pVect(0,0,-1));
      user_rot_axis *= invert(rotall);
      eye_direction *= pMatrix_Rotation(user_rot_axis, M_PI * 0.03);
    }

  // Update eye_location based on keyboard command.
  //
  if ( adjustment.x || adjustment.y || adjustment.z )
    {
      if ( opt_move_item != MI_Ball && opt_move_item != MI_Ball_V )
        global_transform_stale = true;
      const double angle =
        fabs(eye_direction.y) > 0.99
        ? 0 : atan2(eye_direction.x,-eye_direction.z);
      pMatrix_Rotation rotall(pVect(0,1,0),-angle);
      adjustment *= rotall;

      switch ( opt_move_item ){
      case MI_Light: light_location += adjustment; break;
      case MI_Eye: eye_location += adjustment; break;
      case MI_Ball: sphere_location += adjustment; break;
      default: break;
      }
    }
}


int
main(int argv, char **argc)
{
  pVulkan_Helper pvulkan_helper(argv,argc);
  World world(pvulkan_helper);

  world.setup_and_run();

  return 0;
}