/// LSU EE 4702-1 (Fall 2023), GPU Programming
//
//
 /// Homework 1 -- SOLUTION
 //
 //  Assignment: https://www.ece.lsu.edu/gpup/2023/hw01.pdf
 //
 //  This code is based on the solution to 2022 Homework 1.
 //  For the solution to 2022 Homework 2 look 
 //  at file ../../2022/hw01/hw01-sol.cc
 //  or visit https://www.ece.lsu.edu/koppel/gpup/2022/hw01-sol.cc.html
 //

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

// Definitions for iVect, iCoor
#include "hw01-misc.h"


struct HW01_Data {
  HW01_Data():inited(false){};
  bool inited;
  bool tryout0, tryout1;
  vector<uint32_t> fb_area_dup;
  iVect mag_from_diag;
  iVect mag_to_of; // Relative to mouse location.
  iVect mag_to_diag;
};

void
line_draw
( pFrame_Buffer& fb, iCoor p0, iCoor p1, uint32_t co)
{
  int x0 = p0.x, y0 = p0.y;
  int x1 = p1.x, y1 = p1.y;

  const iVect delta = p1 - p0;
  const int width = fb.width_get();

  if ( abs( delta.x ) < abs( delta.y ) )
    {
      // Iterate over y
      if ( y0 > y1 ) { swap(x0,x1); swap(y0,y1); }

      float dxdy = float(delta.x) / delta.y;
      float x = x0;

      for ( int yi = y0;  yi < y1;  yi++ )
        {
          fb[ yi * width + x ] = co;
          x += dxdy;
        }
    }
  else
    {
      // Iterate over x
      if ( x0 > x1 ) { swap(x0,x1); swap(y0,y1); }

      float dydx = float(delta.y) / delta.x;
      float y = y0;

      for ( int xi = x0;  xi < x1;  xi++ )
        {
          fb[ int(y) * width + xi ] = co;
          y += dydx;
        }
    }
}


void
parallel_lines_draw
( pFrame_Buffer& fb, iCoor p0, iCoor pb, iVect v, uint32_t co)
{
  line_draw( fb, p0, p0 + v, co );
  line_draw( fb, pb, pb + v, co );
}

void
parallelogram_draw
( pFrame_Buffer& fb, iCoor p00, iVect vx,  iVect vy, uint32_t color )
{
  parallel_lines_draw(fb, p00, p00+vy, vx, color);
  parallel_lines_draw(fb, p00, p00+vx, vy, color);
}

void
aa_rectangle_draw
( pFrame_Buffer& fb, iCoor p00, int wd, int ht, uint32_t color )
{
  // Window Size, in Pixels
  //
  const int win_width [[maybe_unused]] = fb.width_get();
  const int win_height [[maybe_unused]] = fb.height_get();

  // Compute coordinate of upper-right corner.
  //
  iCoor p11 = p00 + iVect(wd,ht);
  //
  // Coordinates p00 and p11 may be outside of the window.

  /// SOLUTION -- Problem 1

  // Note: The solution below is for cases where wd and ht are positive.
  assert( wd >= 0 && ht >= 0 );  // Raise a fatal error if they are not.

  // Check whether rectangle will be completely outside the window.
  //
  if ( p00.x >= win_width || p00.y >= win_height ) return;
  if ( p11.x <= 0 || p11.y <= 0 ) return;

  // Compute clipped rectangle lower-left coordinate.
  //
  iCoor p00c( max( 0, p00.x ), max( 0, p00.y ) );
  //
  // Note: The /clipped/ rectangle is the portion of the rectangle
  // that is within the window (frame buffer).

  // Compute clipped rectangle upper-right coordinate.
  //
  iCoor p11c( min( win_width-1, p11.x ), min( win_height-1, p11.y ) );

  // Compute the other two clipped rectangle coordinates.
  //
  iCoor p01c( p00c.x, p11c.y ), p10c( p11c.x, p00c.y );

  // Draw visible vertical lines.
  //
  if ( p00.x == p00c.x ) line_draw( fb, p00c, p01c, color );
  if ( p11.x == p11c.x ) line_draw( fb, p10c, p11c, color );

  // Draw visible horizontal lines.
  //
  if ( p00.y == p00c.y ) line_draw( fb, p00c, p10c, color );
  if ( p11.y == p11c.y ) line_draw( fb, p01c, p11c, color );

}

void kbd_input_handle(pFrame_Buffer& fb, HW01_Data& hw01_data);


void
render_hw01(pFrame_Buffer& fb, HW01_Data& hw01_data)
{
  // Window Size, in Pixels
  //
  const int win_width = fb.width_get();
  const int win_height = fb.height_get();

  // Adjust size and position of mag window based on keyboard input.
  kbd_input_handle(fb,hw01_data);

  /// Coordinates Used for Placement of Other Objects
  //
  const int sq_slen = min(win_width,win_height) * .45;
  const int sq_x0 = sq_slen * 0.1,   sq_y0 = win_height - 150;
  const int sq_x1 = sq_x0 + sq_slen, sq_y1 = sq_y0 - sq_slen;
  //
  // One vertex of the square is at (sq_x0,sq_y0) and the opposite
  // vertex is at (sq_x1,sq_y1).

  //                           RRGGBB
  const uint32_t color_red = 0xff0000;
  const uint32_t color_blue =    0xff;
  const uint32_t color_green = 0xff00;

  /// Demo Code: Draw Sine Waves  -- No need to modify this.
  //
  int num_waves = 4;
  float plot_ht = sq_slen/float(num_waves);
  for ( float
          yb = sq_y0 - plot_ht/2,  freq = 4 * M_PI / ( win_width - 40 - sq_x1 );
        yb > sq_y1;
        yb -= plot_ht,  freq *= 2 )
    for ( int x = 20;  x < win_width - 20;  x++ )
      {
        int y = yb + sin( x * freq ) * plot_ht * .45;
        fb[ y * win_width + x ] = 0xff00;
      }

  /// Demo Code: Draw Lines Radiating From a Point -- No need to modify.
  //
  //  This code calls the line_draw routine.
  //
  int n_rects = 24;
  int n_pts = n_rects * 3;
  float c_r1 = sq_slen/6;  // Inner radius
  float c_r2 = sq_slen/2;  // Outer radius.
  pCoor c_ctr( c_r2 + 20, c_r2 + 20 ); // Center of "circle"
  float delta_theta = 2 * M_PI / n_pts;
  vector<pVect> vecs;
  for ( int i=0; i<n_pts; i++ )
    vecs.push_back( pVect( cosf(i*delta_theta), sinf(i*delta_theta), 0 ) );
  for ( int i=0; i<n_pts; i+=3 )
    {
      pVect v1 = vecs[i+1] * ( c_r2 - c_r1 );
      pVect v2 = c_r1 * ( vecs[i+2] - vecs[i] );
      pCoor p1 = c_ctr + c_r1 * vecs[i];
      parallelogram_draw(fb, p1, v1, v2, 0xffffff );
    }

  fb.fb_stats_off(); // Turn off "redundancy" statistics.

  /// Homework 1 Problem 2

  // The code below draws two boxes in the vicinity of the mouse
  // pointer, a blue box, and a green box. Write code that copies the
  // area bounded by the blue box to the area bounded by the green
  // box. Stretch (zoom) or shrink (unzoom) the material in the blue
  // box so that it fills the entire area of the green box. Use the
  // provided staging array, fb_area_dup, so that the green box shows
  // the contents of the blue box *before the copy was started*.

  // Coordinates of mouse pointer.
  //
  const iCoor mouse(fb.mouse_x,fb.mouse_y);

  const iVect& mag_from_diag = hw01_data.mag_from_diag;
  iVect& mag_to_of = hw01_data.mag_to_of;
  iVect& mag_to_diag = hw01_data.mag_to_diag;
  const int mag_to_wd = mag_to_diag.x;
  const int mag_to_ht = mag_to_diag.y;

  // Lower-left of area to be magnified.
  const iCoor mag_from_ll = mouse - mag_from_diag / 2;
  const int mag_from_wd [[maybe_unused ]] = mag_from_diag.x;
  const int mag_from_ht [[maybe_unused]] = mag_from_diag.y;

  // Lower-left of area showing magnification.
  const iCoor mag_to_ll = mag_from_ll + mag_to_of;

  /// Put solution to Problem 2  in this general region.

  const bool show_starter_code = hw01_data.tryout0;
  if ( show_starter_code )
    {
      // Draw a diagonal blue line of positive slope in mag from area.
      for ( int x=0; x<mag_from_wd; x++ )
        fb[ mag_from_ll.x + x + ( mag_from_ll.y + x ) * win_width ]
          = color_blue;

      // Draw a diagonal green line of negative slope in mag to area.
      for ( int x=0; x<min(mag_to_wd,mag_to_ht); x++ )
        fb[ mag_to_ll.x + x + ( mag_to_ht + mag_to_ll.y - x ) * win_width ]
          = color_green;

      // Draw a diagonal red line of positive slope in staging area.
      for ( int x=0; x<mag_from_wd; x++ )
        hw01_data.fb_area_dup[ x + x * mag_from_wd ]
          = color_red;
    }

  /// SOLUTION -- Problem 2
  //
  // Copy the mag_from area to the interim storage area, fb_area_dup.
  //
  for ( int x=0; x<mag_from_wd; x++ )
    for ( int y=0; y<mag_from_ht; y++ )
      {
        // Coordinates of the pixel in the mag_from area.
        //
        iCoor from = mag_from_ll + iVect(x,y);

        // If false, the coordinate is outside the window, and so it
        // won't be copied.
        //
        bool in_win = from.x >= 0 && from.x < win_width
          && from.y >= 0 && from.y < win_height;

        // Write the interim storage with the pixel if its coordinate
        // is valid, write zero otherwise.
        //
        hw01_data.fb_area_dup[ x + y * mag_from_wd ] =
          in_win ? fb[ from.x + from.y * win_width ] : 0;
      }
  //
  // Note that the frame buffer is copied before the blue and green
  // rectangles are drawn.

  // Draw a blue rectangle around area to be magnified.
  aa_rectangle_draw
    ( fb, mag_from_ll, mag_from_diag.x, mag_from_diag.y, color_blue );

  // Compute zoom (magnification) from mag_from (blue) to mag_to (green)
  // areas along each axis.
  //
  const float zoom_x = float(mag_to_wd)/mag_from_wd;
  const float zoom_y = float(mag_to_ht)/mag_from_ht;

  for ( int x=0; x<mag_from_wd; x++ )
    for ( int y=0; y<mag_from_ht; y++ )
      {
        uint32_t pixel = hw01_data.fb_area_dup[x + y * mag_from_wd];

        // Compute range of coordinates in to_area to write into.
        //
        int xt0 = max( 0,          mag_to_ll.x + int(x * zoom_x)     );
        int xt1 = min( win_width,  mag_to_ll.x + int((x+1) * zoom_x) );
        int yt0 = max( 0,          mag_to_ll.y + int(y * zoom_y)     );
        int yt1 = min( win_height, mag_to_ll.y + int((y+1) * zoom_y) );

        for ( int xt = xt0;  xt < xt1; xt++ )
          for ( int yt = yt0; yt < yt1; yt++ )
            fb[ xt + yt * win_width ] = pixel;
      }

  // Draw a green rectangle area to show magnification.
  aa_rectangle_draw
    ( fb, mag_to_ll, mag_to_diag.x, mag_to_diag.y, color_green );

}

void
kbd_input_handle(pFrame_Buffer& fb, HW01_Data& hw01_data)
{
  iVect& mag_to_of = hw01_data.mag_to_of;
  iVect& mag_to_diag = hw01_data.mag_to_diag;

  if ( !hw01_data.inited )
    {
      hw01_data.inited = true;
      hw01_data.tryout0 = false;
      hw01_data.tryout1 = false;
      hw01_data.mag_from_diag = { 100, 100 };
      const int mag_from_wd = hw01_data.mag_from_diag.x;
      const int mag_from_ht = hw01_data.mag_from_diag.y;
      hw01_data.fb_area_dup.resize( mag_from_wd * mag_from_ht );
      mag_to_diag = iVect( mag_from_wd * 2.5, mag_from_ht * 2.5 );
      mag_to_of = iVect( mag_from_wd*.3, mag_from_ht*.7 );
    }

  const int kbd_inc = 2;

  switch ( fb.keyboard_key ) {
  case FB_KEY_RIGHT: case FB_KEY_LEFT:
    {
      const int inc = fb.keyboard_key == FB_KEY_LEFT ? -kbd_inc : kbd_inc;
      if ( fb.keyboard_shift ) mag_to_of.x += 2*inc;
      else { mag_to_diag.x += 2*inc; mag_to_of.x -= inc; }
      break;
    }
  case FB_KEY_UP: case FB_KEY_DOWN:
    {
      const int inc = fb.keyboard_key == FB_KEY_DOWN ? -kbd_inc : kbd_inc;
      if ( fb.keyboard_shift ) mag_to_of.y += 2*inc;
      else { mag_to_diag.y += 2*inc; mag_to_of.y -= inc; }
      break;
    }
  case 'y':
    hw01_data.tryout0 = !hw01_data.tryout0; break;
  case 'Y':
    hw01_data.tryout1 = !hw01_data.tryout1; break;
  default:
    break;
  };


}

int
main(int argc, char **argv)
{
  pFrame_Buffer demo_frame_buffer(argc,argv);

  HW01_Data hw01_data;
  demo_frame_buffer.show
    ( [&](pFrame_Buffer& fb){ render_hw01(fb,hw01_data); } );
  return 0;
}