/// LSU EE 4702-1 (Fall 2024), GPU Programming
//
 /// Homework 4 -- SOLUTION
//
//   Modify this file AND hw04-shdr.cc
//   Assignment: https://www.ece.lsu.edu/koppel/gpup/2024/hw04.pdf



// Include files provided for this course.
//
#define MAIN_INCLUDE
#include <vhelper.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"

struct Uni_Light_Simple {
  vec4 position;
  vec4 color;
};

struct Uni_Misc {
  ivec4 opt_tryout;
};


constexpr int ncolors = 10;
struct HW03_Colors {
  pColor front[ncolors], back[ncolors];
};


struct Hyperboloid_Info {

  pCoor bot_ctr_pos, top_ctr_pos;
  pCoor line_0_bot_pos;
  pVect line_0_dir;

  int n_sides_constructed;
  int serial;
};


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

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

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

  pCoor eye_location, eye_initial_location;
  pVect eye_direction;
  pNorm eye_initial_direction;

  VTransform transform;
  VBufferV<Uni_Light_Simple> uni_light_simple;
  VBufferV<Uni_Misc> uni_misc;
  VPipeline pipe_plain;
  VVertex_Buffer_Set bset_plain;

  bool opt_tryout1, opt_tryout2;  // For ad-hoc experiments.

  void scene_setup_1();
  void scene_setup_2();
  double world_time, time_last_update;
  bool opt_pause;

  VBufferV<HW03_Colors> uni_hw03_colors;

  /// 2024 Homework 4

  enum Pipeline_Variant { PV_Individ, PV_Strip, PV_SIZE };
  int pipeline_variant;

  Hyperboloid_Info hyperb_info;
  void hyperb_setup(bool r);
  VPipeline pipe_hyperb;
  VPipeline pipe_hyperb_strip;
  VVertex_Buffer_Set bset_hyperb;
  int hyperb_serial;
  int hyperb_setup_n;

  int opt_n_sides;

};

const char* const pipeline_variant_str[] =
  { "INDIVID", "STRIP" };



void
World::scene_setup_1()
{
  hyperb_setup(false);
}

void
World::scene_setup_2()
{
  hyperb_setup(true);
}

float
prand(float max)
{
  double r = random() * (1.0 / RAND_MAX );
  return r * max;
}

float
prand(float min, float max)
{
  assert( min <= max );
  return min + prand( max - min );
}

pVect
norm_rand()
{
  double theta = prand(2*M_PI);
  double eta = prand(2*M_PI);
  pVect az(0,0,1);
  pVect a = pMatrix_Rotation(az,theta) * pVect(1,0,0);
  pVect a2 = cross(az,a);
  pVect b = pMatrix_Rotation(a2,eta) * a;
  float l = b.mag();
  assert( fabs(l-1) < 0.0001 );
  return b;
}

pVect vec_rand(float r1) { return prand(r1) * norm_rand(); }
pVect vec_rand(float r1, float r2) { return prand(r1,r2) * norm_rand(); }

pVect vec_rand(float rmin, float rmax, pVect axis)
{
  pNorm v = pMatrix_Rotation(axis.normal(),prand(2*M_PI)) * axis.orthogonal();
  return prand(rmin,rmax) * v;
}


void
World::hyperb_setup(bool reset)
{
  world_time = 0;
  time_last_update = time_wall_fp();
  if ( reset ) hyperb_setup_n = 0;
  hyperb_setup_n++;

  Hyperboloid_Info& h = hyperb_info;
  float s = 0.6;

  h.n_sides_constructed = 0;

  switch ( hyperb_setup_n ) {
  case 1:
    // Largish
    h.top_ctr_pos    = pCoor(2.78,0.83,0.89);
    h.bot_ctr_pos    = pCoor(2.68,-1.03,0.52);
    h.line_0_bot_pos = pCoor(2.82,-0.35,-2.95);
    h.line_0_dir     = pNorm(0.67,0.27,0.69);
    break;
  case 2:
    // Food display shape.
    h.top_ctr_pos    = pCoor(2.95,0.42,-0.31);
    h.bot_ctr_pos    = pCoor(2.96,-0.85,-0.42);
    h.line_0_bot_pos = pCoor(2.20,-0.68,-2.47);
    h.line_0_dir     = pNorm(0.69,0.18,0.70);
    break;

  default:
    hyperb_info.bot_ctr_pos = pCoor(3,-1,0) + vec_rand(1);
    h.top_ctr_pos =
      h.bot_ctr_pos + pVect(0,prand(1*s,5*s),0) + vec_rand(0,s,pVect(0,1,0));
    pVect vz(h.bot_ctr_pos,h.top_ctr_pos);
    h.line_0_bot_pos = h.bot_ctr_pos + vec_rand(2*s,6*s,vz);
    pCoor line_0_top_pos = hyperb_info.top_ctr_pos + vec_rand(2*s,6*s,vz);
    hyperb_info.line_0_dir = pNorm(h.line_0_bot_pos,line_0_top_pos);
    break;
  }

  printf("h.top_ctr_pos = pCoor(%.2f,%.2f,%.2f);\n",
         h.top_ctr_pos.x, h.top_ctr_pos.y,h.top_ctr_pos.z);
  printf("h.bot_ctr_pos = pCoor(%.2f,%.2f,%.2f);\n",
         h.bot_ctr_pos.x, h.bot_ctr_pos.y,h.bot_ctr_pos.z);
  printf("h.line_0_bot_pos = pCoor(%.2f,%.2f,%.2f);\n",
         h.line_0_bot_pos.x, h.line_0_bot_pos.y,h.line_0_bot_pos.z);
  printf("h.line_0_dir = pNorm(%.2f,%.2f,%.2f);\n",
         h.line_0_dir.x, h.line_0_dir.y,h.line_0_dir.z);




}


void
World::setup_and_run()
{
  // Setup Vulkan context, etc.
  vh.init();
  vh.display_cb_set([&](){});
  vh.cbs_cmd_record.push_back( [&](vk::CommandBuffer& cb){ render(cb); });

  pipeline_variant = 0;
  opt_tryout1 = opt_tryout2 = false;
  eye_initial_location = pCoor( 3, .5, 8.6) ;
  eye_location = eye_initial_location;
  eye_initial_direction = pVect(0,0,-1);
  eye_direction = eye_initial_direction;

  opt_light_intensity = 22;
  light_location = pCoor( -6.6, 8.0, 2.5 );

  opt_move_item = MI_Eye;

  uni_misc.init(vh.qs,vk::BufferUsageFlagBits::eUniformBuffer);
  uni_misc->opt_tryout = ivec4(opt_tryout1,opt_tryout2,0,0);

  uni_light_simple.init(vh.qs,vk::BufferUsageFlagBits::eUniformBuffer);

  uni_hw03_colors.init(vh.qs,vk::BufferUsageFlagBits::eUniformBuffer);

  hyperb_info.serial = 0;
  hyperb_serial = -1;
  hyperb_setup_n = 0;
  opt_n_sides = 49;
  variable_control.insert(opt_n_sides,"n_sides",1,3);

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

  opt_pause = false;
  scene_setup_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_simple.destroy();
  uni_misc.destroy();
  pipe_plain.destroy();
  bset_plain.destroy();
  shapes.destroy();
  transform.destroy();

  uni_hw03_colors.destroy();

  // 2024 Homework 4
  pipe_hyperb.destroy();
  pipe_hyperb_strip.destroy();
  bset_hyperb.destroy();

  vh.finish();
}

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

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

  /// 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*2) & 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]\n",
     light_location.x, light_location.y, light_location.z);

  vh.fbprintf
    ("Pipeline Variant: %s ('v')  Tryout 1: %s  ('y')  Tryout 2: %s  ('Y')  \n",
     pipeline_variant_str[pipeline_variant],
     opt_tryout1 ? BLINK("ON ","   ") : "OFF",
     opt_tryout2 ? 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
  ///

  pMatrix eye_from_global =
    pMatrix_Rotation(eye_direction,pVect(0,0,-1))
    * pMatrix_Translate(-eye_location);

  transform.eye_from_global_set( eye_from_global );

  /// Setup Projection Transformation:  Eye Space -> Clip Space
  //
  const int win_width = vh.s_extent.width;
  const int win_height = vh.s_extent.height;
  const float aspect = float(win_width) / win_height;
  const float n_dist = 0.01;
  const float xr = .8 * n_dist;

  // Frustum: left, right, bottom, top, near, far
  transform.clip_from_eye_set
    ( pMatrix_Frustum( -xr, xr,                // left, right
                       -xr/aspect, xr/aspect,  // bottom, top
                       n_dist, 5000            // near, far 
                       ) );

  /// Lighting
  //
  uni_light_simple->color = color_white * opt_light_intensity;
  uni_light_simple->position = transform.eye_from_global * light_location;
  uni_light_simple.to_dev();

  uni_misc.to_dev();

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

  if ( !pipe_plain )
    pipe_plain
      .init( vh.qs )
      .ds_uniform_use( "BIND_LIGHT_SIMPLE", uni_light_simple )
      .ds_uniform_use( "BIND_MISC", uni_misc )
      .shader_inputs_info_set<pCoor,pNorm,pColor>()
      .shader_code_set
      ("demo-03-shdr.cc", "vs_main();", nullptr, "fs_main();")
      .topology_set( vk::PrimitiveTopology::eTriangleList )
      .create();

  // Reset buffers in this buffer set.
  //
  bset_plain.reset(pipe_plain);

  // Object-space coordinates of a triangle.
  //
  pCoor p0 = { 0, 0,  0 };
  pCoor p1 = { 9, 6, -9 };
  pCoor p2 = { 0, 5, -5 };

  // Find triangle normal using cross product function (in coord.h).
  //
  pNorm tri_norm = cross( p0, p1, p2 );
  pColor color_tri( .2, .900, .1 ); // Red, Green, Blue; Values in [0,1]

  // Insert vertex coordinates into buffer set (bset_plain).
  //
  bset_plain << p0 << p1 << p2;

  // Insert vertex colors and normals.
  //
  bset_plain << color_tri << color_red << color_blue;
  bset_plain << tri_norm << tri_norm << tri_norm;
  //
  // All three vertices here have the same normal.

  // Add a square consisting of a red and green triangle.
  //
  bset_plain << pCoor(-2,0,-2) << pCoor(-4,2,-2) << pCoor(-4,0,-2);
  bset_plain << color_red << color_red << color_red;
  bset_plain << pCoor(-2,0,-2) << pCoor(-2,2,-2) << pCoor(-4,2,-2);
  bset_plain << color_green << color_green << color_green;
  pNorm snorm = cross( pCoor(-4,0,-2), pCoor(-4,2,-2), pCoor(-2,0,-2) );
  bset_plain << snorm << snorm << snorm << snorm << snorm << snorm;

  bset_plain.to_dev();
  pipe_plain.ds_set( transform );
  pipe_plain.record_draw(cb, bset_plain);

  // Update theta_0, the angle by which the turbines should be at.
  //
  if ( !opt_pause ) world_time += time_now - time_last_update;
  time_last_update = time_now;
  const float omega = 1;
  const float theta_0 = world_time * omega;

  Hyperboloid_Info& h = hyperb_info;

  // Values used in constructing the turbines.
  //

  if ( true )
    {
      uni_hw03_colors->front[0] = color_red;
      uni_hw03_colors->back[0] = color_red;

      uni_hw03_colors->front[1] = color_lsu_spirit_purple;
      uni_hw03_colors->back[1] = color_lsu_spirit_gold;
      uni_hw03_colors->front[2] = color_lsu_business_purple;
      uni_hw03_colors->back[2] = color_gold;
      uni_hw03_colors->front[3] = color_salmon;
      uni_hw03_colors->back[3] = color_rosy_brown;
      uni_hw03_colors->front[4] = color_aquamarine;
      uni_hw03_colors->back[4] = color_peru;;

      uni_hw03_colors->front[5] = color_navajo_white;
      uni_hw03_colors->back[5] = color_white;

      uni_hw03_colors.to_dev();

    }

  if ( !pipe_hyperb )
    pipe_hyperb
      .init( vh.qs )
      .ds_uniform_use( "BIND_LIGHT_SIMPLE", uni_light_simple )
      .ds_uniform_use( "BIND_MISC", uni_misc )
      .ds_uniform_use( "BIND_HW03", uni_hw03_colors )
      .shader_inputs_info_set<pCoor,pNorm,int>()
      .shader_code_set
      ("hw04-shdr-sol.cc", "vs_main();", "gs_main();", "fs_main();")
      .topology_set( vk::PrimitiveTopology::eTriangleList )
      .create();

  if ( !pipe_hyperb_strip )
    pipe_hyperb_strip
      .init( vh.qs )
      .ds_uniform_use( "BIND_LIGHT_SIMPLE", uni_light_simple )
      .ds_uniform_use( "BIND_MISC", uni_misc )
      .ds_uniform_use( "BIND_HW03", uni_hw03_colors )
      .shader_inputs_info_set<pCoor,pNorm,int>()
      .shader_code_set
      ("hw04-shdr-sol.cc", "vs_main();", "gs_main();", "fs_main();")
      .topology_set( vk::PrimitiveTopology::eTriangleStrip )
      .create();

  if ( h.n_sides_constructed != opt_n_sides ) h.serial++;

  pCoor bot_ctr = h.bot_ctr_pos;
  pCoor top_ctr = h.top_ctr_pos;
  pCoor line_0_bot_pos = h.line_0_bot_pos;
  pVect line_0_dir = hyperb_info.line_0_dir;

  pVect hyperb_vz(bot_ctr,top_ctr);
  pNorm hyperb_az(hyperb_vz);
  pVect bot_vx(bot_ctr,line_0_bot_pos);

  pMatrix_Rotation rot(hyperb_az,theta_0);
  pMatrix trot =
    pMatrix_Translate( bot_ctr ) * rot * pMatrix_Translate( -bot_ctr );

  if ( !bset_hyperb || hyperb_serial != h.serial )
    {
      hyperb_serial = h.serial;
      int n_levels = opt_n_sides;
      int n_sides = opt_n_sides;
      h.n_sides_constructed = n_sides;

      pCoor line_0_top_pos =
        line_0_bot_pos
        + hyperb_az.magnitude / dot( hyperb_az, line_0_dir ) * line_0_dir;

      float delta_theta = 2 * M_PI / n_sides;
      pVect delta_z = (1.0/n_levels ) * hyperb_vz;

      pVect line_v(line_0_bot_pos,line_0_top_pos);
      pVect line_dv = (1.0/n_levels) * line_v;

      vector< vector<pCoor> > coords(n_levels+1);
      vector< vector<pNorm> > norms(n_levels+1);

      pVect top_vx(top_ctr,line_0_top_pos);

      for ( int lev = 0; lev <= n_levels; lev++ )
        {
          coords[lev].resize(n_sides+1);
          norms[lev].resize(n_sides+1);
        }

      // Compute vertex coordinates and normals and write them
      // to CPU containers coords and norms.
      //
      for ( int side = 0; side <= n_sides; side++ )
        {
          float theta = side * delta_theta;
          pMatrix_Rotation rot(hyperb_az,theta);
          pCoor line_bot_pos = bot_ctr + rot * bot_vx;
          pVect l_dv = rot * line_dv;

          for ( int lev = 0; lev <= n_levels; lev++ )
            {
              pCoor p_line = line_bot_pos + lev * l_dv;
              coords[lev][side] = p_line;
              pCoor ctr = bot_ctr + lev * delta_z;
              pVect tc = cross(hyperb_az,pVect(ctr,p_line));
              pNorm n = cross(tc,l_dv);
              norms[lev][side] = n;
            }
        }

      switch ( pipeline_variant ) {
      case PV_Individ:

        bset_hyperb.reset(pipe_hyperb);
        for ( int side = 0; side < n_sides; side++ )
          for ( int lev = 0; lev < n_levels; lev++ )
            {
              pCoor p00 = coords[lev][side];
              pCoor p01 = coords[lev][side+1];
              pCoor p10 = coords[lev+1][side];
              pCoor p11 = coords[lev+1][side+1];

              bset_hyperb << p00 << p01 << p10;
              bset_hyperb << p01 << p11 << p10;

              pNorm n00 = norms[lev][side];
              pNorm n01 = norms[lev][side+1];
              pNorm n10 = norms[lev+1][side];
              pNorm n11 = norms[lev+1][side+1];

              bset_hyperb << n00 << n01 << n10;
              bset_hyperb << n01 << n11 << n10;

              int c1 = side == 0 ? ( lev%2 ? 5 : 2 ) : 1;
              int c2 = side == 0 ? ( lev%2 ? 3 : 4 ) : 1;

              bset_hyperb << c1 << c1 << c1 << c2 << c2 << c2;
            }
        break;

      case PV_Strip:

        bset_hyperb.reset(pipe_hyperb_strip);

        /// Homework 4
        //
        //  Modify code in this switch and elsewhere if necessary to
        //  properly use triangle strip. Write vertex attributes to
        //  bset_hyperb.

        /// SOLUTION -- Problem 1
        //
        //  Each side loop iteration emits one triangle strip iteration.
        //  The lev loop ends at n_levels rather than n_levels.

        for ( int side = 0; side < n_sides; side++ )
          for ( int lev = 0; lev <= n_levels; lev++ )
            {
              // For triangle strip just emit two vertices per iteration ..
              // .. rather than six vertices. 
              // Same two triangles, but using four fewer vertices!

              pCoor p00 = coords[lev][side];
              pCoor p01 = coords[lev][side+1];

              bset_hyperb << p00 << p01;

              pNorm n00 = norms[lev][side];
              pNorm n01 = norms[lev][side+1];

              bset_hyperb << n00 << n01;

              // Choose color index.
              //
              int c1 = lev == 0 ? 0 : side == 0 ? ( (lev+1)%2 ? 5 : 2 ) : 1;
              int c2 = lev == 0 ? 0 : side == 0 ? ( (lev+1)%2 ? 3 : 4 ) : 1;
              //
              // If lev == 0, set to zero to start triangle strip;
              // if side == 0, choose from among four colors to show structure;
              // otherwise, use color index 1.

              bset_hyperb << c1 << c2;
            }

        break;

      default: assert( false );
      }

      bset_hyperb.to_dev();
    }

  switch ( pipeline_variant ) {
  case PV_Individ:
    pipe_hyperb.ds_set( transform * trot );
    pipe_hyperb.record_draw(cb, bset_hyperb);
    break;
  case PV_Strip:
    pipe_hyperb_strip.ds_set( transform * trot );
    pipe_hyperb_strip.record_draw(cb, bset_hyperb);
    break;
  default: assert( false );
  }


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

}



void
World::keyboard_handle()
{
  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: user_rot_axis.x = 1; break;
  case FB_KEY_END: user_rot_axis.x = -1; break;
  case 'n': bset_hyperb.reset(); break;
  case '1': scene_setup_1(); break;
  case '2': scene_setup_2(); 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 'p': case 'P': opt_pause = !opt_pause; break;
  case 'v': case 'V':
    hyperb_info.serial++;
    if ( ++pipeline_variant >= PV_SIZE ) pipeline_variant = 0;
    break;
  case 'y': opt_tryout1 = !opt_tryout1;
    uni_misc->opt_tryout.x = int(opt_tryout1);
    break;
  case 'Y': opt_tryout2 = !opt_tryout2;
    uni_misc->opt_tryout.y = int(opt_tryout2);
    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 )
    {
      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: object_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;
}