/// LSU EE 4702-1 (Fall 2024), GPU Programming
//
 /// Rendering Pipeline
//

#include <gp/coord.h>
#include <gp/colors.h>
#include "frame_buffer.h"

 ///
 /// Our_3D: Low-Level 3D Graphics Library.
 ///
class Our_3D {
public:
  Our_3D(pFrame_Buffer& fbp)
    :frame_buffer(fbp)
  {
    vtx_colors = nullptr;
    vtx_coors = nullptr;
    vtx_normals = nullptr;
    topology_strip = false;
    opt_tri_normals_always = false;
  };

  Our_3D& transform_eye_from_object_set(const pMatrix& m)
  {
    eye_from_object = m;
    return *this;
  }
  Our_3D& transform_clip_from_eye_set(const pMatrix& m)
  {
    clip_from_eye = m;
    return *this;
  }
  Our_3D& light_location_set(const pCoor c)
  {
    light_location = c;
    return *this;
  }
  Our_3D& light_color_set(const pColor c)
  {
    light_color = c;
    return *this;
  }

  Our_3D& topology_strip_set( bool strip = true )
  {
    topology_strip = strip;
    return *this;
  }

  Our_3D& vtx_normals_set()
  {
    vtx_normals = nullptr;
    return *this;
  }

  Our_3D& vtx_normals_set(vector<pVect4>& c)
  {
    vtx_normals = c.data();
    return *this;
  }

  Our_3D& vtx_coors_set(vector<pCoor>& c)
  {
    vtx_coors = &c;
    return *this;
  }

  Our_3D& vtx_colors_set(vector<pColor>& c)
  {
    vtx_colors = &c;
    return *this;
  }

  Our_3D& draw_rasterization();

  pFrame_Buffer& frame_buffer;
  pMatrix eye_from_object, clip_from_eye;
  pCoor light_location;
  pColor light_color;
  vector<pCoor> *vtx_coors;
  pVect4 *vtx_normals;
  vector<pColor> *vtx_colors;
  bool topology_strip;
  bool opt_tri_normals_always;
  bool opt_tryout1, opt_tryout2;

};


 ///
 ///  World: An application.
 ///
class World : public World_Base {
public:
  World(pFrame_Buffer& fb):World_Base(fb),gc(fb){}
  Our_3D gc;

  // Called each time a frame needs to be rendered.
  virtual void render(bool first_time)
  {
    if ( first_time ) init_scene();
    render_scene();
  }

  // Called Once
  void init_scene();

  // Called for each frame.
  void render_scene();

  pCoor eye_location, light_location;
  pColor light_color;
  pCoor *adj_location;
  pVect eye_direction;
  bool opt_tryout1,opt_tryout2;

};

void
World::init_scene()
{
  eye_location = pCoor(-1.2,.5,10.2);
  light_location = pCoor(-2.2,1.5,10.2);
  eye_direction = pVect(.1,0,-1);
  adj_location = &eye_location;
  opt_tryout1 = opt_tryout2 = false;
  gc.opt_tryout1 = opt_tryout1;
  gc.opt_tryout2 = opt_tryout2;
  light_color = color_white * 100;
}

void
World::render_scene()
{
  pFrame_Buffer& frame_buffer = gc.frame_buffer;

  switch ( frame_buffer.keyboard_key ) {
  case FB_KEY_LEFT: adj_location->x -= 0.1; break;
  case FB_KEY_RIGHT: adj_location->x += 0.1; break;
  case FB_KEY_PAGE_UP: adj_location->y += 0.1; break;
  case FB_KEY_PAGE_DOWN: adj_location->y -= 0.1; break;
  case FB_KEY_UP: adj_location->z -= 0.1; break;
  case FB_KEY_DOWN: adj_location->z += 0.1; break;
  case 'e': case 'E': adj_location = &eye_location; break;
  case 'l': case 'L': adj_location = &light_location; break;
  case 'v': case 'V': gc.opt_tri_normals_always = !gc.opt_tri_normals_always;
    break;
  case 'y': gc.opt_tryout1 = opt_tryout1 = !opt_tryout1; break;
  case 'Y': gc.opt_tryout2 = opt_tryout2 = !opt_tryout2; break;
  case '+': case '=':
    if ( !frame_buffer.keyboard_control ) light_color *= 1.1;
    break;
  case '-':
    if ( !frame_buffer.keyboard_control ) light_color *= 1/1.1;
    break;

  }

  gc.frame_buffer.fbprintf
    ( "Light Location [ %.2f, %.2f, %.2f ],  Light Intensity %.2f ('+-')  "
      "Eye Location [ %.2f, %.2f, %.2f ]\n",
      light_location.x, light_location.y, light_location.z,
      light_color.r,
      eye_location.x, eye_location.y, eye_location.z );
  frame_buffer.fbprintf
    ("Tryout1 %s ('y')  Tryout2 %s ('Y')\n",
     opt_tryout1 ? "ON " : "OFF", opt_tryout2 ? "ON " : "OFF");

  gc.light_location_set( light_location ).light_color_set( light_color );

  ///
  /// Prepare Transformations
  ///

  /// Specifications of User's Monitor
  //
  //  First, the width of the user's monitor in object space coordinates.
  //
  const float width_m = 1.6;  // Wow, that's a big monitor! (m is for meter.)
  const float qn = 1;         // Distance from eye to monitor along z axis.
  //
  //  To determine the height of the user's monitor we need to know the
  //  aspect ratio of the window we are painting.
  //
  const uint win_width = frame_buffer.width_get();
  const uint win_height = frame_buffer.height_get();
  //
  //  win_width and win_height are pixel-space coordinates.
  //
  const float aspect = 1.0 * win_width / win_height;
  //
  //  Using the aspect ratio and the width we can compute the height.
  //
  const float height_m = width_m / aspect;
  //
  //  width_m and height_m are in object-space coordinates.

  // Compute transformation from Object Space to Eye Space ..
  //
  pMatrix_Translate center_eye(-eye_location);
  pMatrix_Rotation rotate_eye(eye_direction,pVect(0,0,-1));
  pMatrix eye_from_object = rotate_eye * center_eye;
  //
  // .. and give the transformation to Our3D:
  //
  gc.transform_eye_from_object_set(eye_from_object);

  // Compute transformation from Eye Space to Clip Space ..
  //
  pMatrix_Frustum clip_from_eye
    ( -width_m/2, width_m/2,  -height_m/2, height_m/2,  qn, 5000 );
  //   l          r           b            t            n   f
  //   The letters l,r,b,t,n,f are from a frustum transform definition.
  //
  // .. and give the transformation to Our3D:
  //
  gc.transform_clip_from_eye_set(clip_from_eye);


  ///
  /// Prepare Triangles
  ///

  // List of coordinates in object space and list of colors.
  //
  vector<pCoor> coors_os;
  vector<pColor> colors;

  // Insert First Triangle
  //
  // Insert coordinates of a triangle into coordinate list,
  // and insert its color into a color list.
  //
  coors_os << pCoor( 0, 0, 0 ) << pCoor( 9, 6, -9 ) << pCoor( 0, 7, -5 );
  colors << color_white << color_white << color_white;

  // Insert Second Triangle
  //
  // Add coordinates and color of another triangle
  //
  coors_os << pCoor(7,4,-2) << pCoor(-3,5,-9) << pCoor(9,2,-2);
  colors << color_blue << color_blue << color_blue;

  // Insert Two More Triangles
  //
  // Add a square consisting of a red and green triangle.
  //
  colors << color_lsu_spirit_gold << color_lsu_spirit_gold
         << color_lsu_spirit_gold;
  colors << color_lsu_spirit_purple << color_lsu_spirit_purple
         << color_lsu_spirit_purple;
  coors_os << pCoor(-7,0,-2) << pCoor(-7,2,-2) << pCoor(-5,0,-2);
  coors_os << pCoor(-7,2,-2) << pCoor(-5,2,-2) << pCoor(-5,0,-2);

  // Insert a triangle at light location, facing user. Light is top point.
  pVect light_to_eye( light_location, eye_location );
  float l_sz = 0.1;
  pVect lax = cross( pVect( 0, 1, 0 ), light_to_eye ).normal() * l_sz;
  pVect lay = cross( light_to_eye, lax ).normal() * l_sz;
  coors_os << light_location
           << light_location - lax - lay << light_location + lax - lay;
  colors << color_yellow << color_yellow << color_yellow;

  ///
  /// Send Triangles to Our3D for Rendering
  ///
  //
  gc.vtx_normals_set();            // Tell Our3D to compute triangle normals.
  gc.topology_strip_set(false);    // Use individual triangle grouping.
  gc.vtx_coors_set(coors_os);      // Coordinates to render.
  gc.vtx_colors_set(colors);       // Colors to render.
  gc.draw_rasterization();         // Tell Our3D to render now.

  ///
  /// Render a Cylinder
  ///

  // First remove the coordinates and colors inserted above.
  //
  coors_os.clear(); colors.clear();

  // Container for vertex normals.
  vector<pVect4> normals_os;

  // Specifications of Cylinder
  //
  pCoor cyl_center(1,-1,-3);
  pVect cyl_axis( 0, 0.5, 0.05 );
  float cyl_radius = 4;
  //
  // Compute axes of circle. (Base of cylinder, top of cylinder.)
  //
  pNorm ax(0,-cyl_axis.z,cyl_axis.y);  // Find an axis orthogonal to cyl_axis.
  pNorm ay = cross( cyl_axis, ax );
  //
  int n_segs = 20;  // Number of faces in prism (approximating a cylinder.)
  //
  float delta_theta = 2 * M_PI / n_segs;

  for ( int i=0; i<=n_segs; i++ )
    {
      float theta = i * delta_theta;
      pVect4 n = ax * cosf(theta) + ay * sinf(theta);
      pCoor c = cyl_center + cyl_radius * n;

      coors_os << c + cyl_axis << c;
      normals_os << n << n;
      colors << color_cyan << color_cyan;
    }

  ///
  /// Send Triangles to Our3D for Rendering
  ///
  //
  gc.vtx_normals_set(normals_os);  // This time we're providing normals.
  gc.topology_strip_set(true);     // Use a triangle strip grouping.
  gc.vtx_coors_set(coors_os);
  gc.vtx_colors_set(colors);
  gc.draw_rasterization();
}

Our_3D&
Our_3D::draw_rasterization()
{
  ///
  /// Prepare Transformations
  ///

  // Specifications of User's Monitor
  //
  const uint win_width = frame_buffer.width_get();
  const uint win_height = frame_buffer.height_get();
  //
  // win_width and win_height are pixel-space coordinates.

  // Transform from Clip to Pixel
  //
  pMatrix_Translate recenter(pVect(1,1,0));
  pMatrix_Scale scale( win_width/2, win_height/2, 1 );
  pMatrix window_from_clip = scale * recenter;
  pMatrix ws_from_os = window_from_clip * clip_from_eye * eye_from_object;

  ///
  /// Rasterize Triangle and Write Frame Buffer
  ///

  // Get pointer to the depth (z) buffer.
  //
  float* const zbuffer = frame_buffer.buffer_depth_get();
  //
  // The depth buffer uses the same index as the frame buffer.

  auto& coors_os = *vtx_coors;
  auto& colors = *vtx_colors;

  const size_t inc = topology_strip ? 1 : 3;
  const size_t stop = coors_os.size() - inc;

  for ( size_t i=0; i<=stop; i += inc )
    {
      // Get next triangle's object space coordinates ..
      //
      pCoor o0 = coors_os[i+0],  o1 = coors_os[i+1],  o2 = coors_os[i+2];
      pCoor_Homogenized_Keep_w w0( ws_from_os * o0 );
      pCoor_Homogenized_Keep_w w1( ws_from_os * o1 );
      pCoor_Homogenized_Keep_w w2( ws_from_os * o2 );

      pColor c0 = colors[i+0], c1 = colors[i+1], c2 = colors[i+2];

      // Compute triangle normal (for lighting).
      //
      pNorm tn = cross(o0,o1,o2);

      // Non-null if user has provided vertex normals and wants to use them.
      pVect4* np = !opt_tri_normals_always && vtx_normals
        ? &vtx_normals[i] : nullptr;

      pVect w20(w2,w0), w21(w2,w1);
      const float db0 = 1/max(fabs(w20.x),fabs(w20.y));
      const float db1 = 1/max(fabs(w21.x),fabs(w21.y));

      // Quick and dirty method of skipping huge triangles.
      if ( db0 * db1 < 1e-7 ) continue;

      // Used for computing perspective-correct barycentric coordinates.
      const float w0ow2 = w0.w/w2.w, w0ow1 = w0.w/w1.w;
      const float w1ow2 = w1.w/w2.w, w1ow0 = w1.w/w0.w;

      // Rasterize the Triangle.
      // Iterate over triangle using barycentric coordinates.
      //
      for ( float b0=0; b0<=1; b0 += db0 )
        for ( float b1=0; b1<=1-b0; b1 += db1 )
          {
            const float b2 = 1 - b0 - b1;

            // Each iteration operates on one fragment of triangle.

            // Window-Space Coordinates of Fragment
            //
            pCoor wf = b0 * w0 + b1 * w1 + b2 * w2;

            if ( uint(wf.x) >= win_width || uint(wf.y) >= win_height ) continue;

            // Compute the index into the frame and depth buffers.
            //
            const size_t idx = wf.x + int(wf.y) * win_width;

            // Depth (Z) Test
            //
            float zinv = wf.z;
            if ( zinv < 0 || zinv > 1 ) continue;
            if ( zbuffer[ idx ] < zinv ) continue;

            zbuffer[ idx ] = zinv;

            // Compute perspective-correct barycentric coordinates.
            //
            float bc0 = b0 / ( b2 * w0ow2 + b1 * w0ow1 + b0 );
            float bc1 = b1 / ( b2 * w1ow2 + b0 * w1ow0 + b1 );
            float bc2 = 1 - bc0 - bc1;

            // Find object-space coordinate of fragment.
            //
            pCoor of =     bc0 * o0 + bc1 * o1 + bc2 * o2;

            // Blend color of three vertices together.
            //
            pColor color = bc0 * c0 + bc1 * c1 + bc2 * c2;

            // If provided, blend vertex normals, otherwise use triangle normal.
            //
            pVect4 n = np ? bc0 * np[0] + bc1 * np[1] + bc2 * np[2] : tn;

            // Compute lighted color.
            //
            pNorm f_to_l(of,light_location);
            float phase = fabs(dot(n,f_to_l));
            pColor lighted_color = phase / f_to_l.mag_sq * light_color * color;

            //
            // Write the frame (color) buffer with the lighted color
            //
            frame_buffer[ idx ] = lighted_color.int_rgb();
          }
    }
  return *this;
}


int
main(int argc, char **argv)
{
  pFrame_Buffer demo_frame_buffer(argc,argv);
  WRender render( demo_frame_buffer );
  World our_world( demo_frame_buffer );
  WRENDER_INSERT(render, our_world);
  render.run();
  return 0;
}