/// LSU EE 4702-1 (Fall 2024), GPU Programming
//
//
 /// Homework 1 -- SOLUTION
 //
 //  Assignment: https://www.ece.lsu.edu/gpup/2024/hw01.pdf
 //
 //  Solution is in routine render_hw01, search for SOLUTION.

#include <span>
#include <gp/coord.h>
#include <gp/texture-util.h>
#include "frame_buffer.h"

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


enum Wrap_Mode { WM_Repeat, WM_Mirror, WM_Transparent, WM_ENUM_SIZE };
const char* wrap_mode_str[] = { "REPEAT", "MIRROR", "TRANSPARENT" };


struct HW01_Data {
  HW01_Data():inited(false){};
  bool inited;
  bool tryout1, tryout2;
  pCoor img_box[4];  // Or, just use floats.
  uint img_box_dragging;
  vector<uint32_t> img_pixels;
  int img_px_width, img_px_height;
  float zoom;
  int wrap_mode;
};

void kbd_input_handle(pFrame_Buffer& fb, HW01_Data& hw01_data);
void image_box_drag(pFrame_Buffer& fb, HW01_Data& hw01_data);
void line_draw( pFrame_Buffer& fb, iCoor p0, iCoor p1, uint32_t co);
void aa_rectangle_draw
( pFrame_Buffer& fb, iCoor p00, int wd, int ht, uint32_t color );
void aa_rectangle_draw
( pFrame_Buffer& fb, iCoor p00, iCoor p11, uint32_t color );
void parallelogram_draw
( pFrame_Buffer& fb, iCoor p00, iVect vx,  iVect vy, uint32_t color );


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();
  const auto& tryout1 [[maybe_unused]] = hw01_data.tryout1;
  const auto& tryout2 [[maybe_unused]] = hw01_data.tryout2;

  kbd_input_handle(fb,hw01_data);
  image_box_drag(fb,hw01_data); // Update image box corners.


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

  /// Homework 1: No need to modify code above.


  /// Image Box Variables
  ///
  const auto& img_box = hw01_data.img_box;
  //
  // Image Box Vertices (Corners)
  //
  //   p3 --- p2
  //   |       |
  //   p0 --- p1
  //
  const pCoor p0 [[maybe_unused]] = img_box[0];
  const pCoor p1 [[maybe_unused]] = img_box[1];
  const pCoor p2 [[maybe_unused]] = img_box[2];
  const pCoor p3 [[maybe_unused]] = img_box[3];

  /// SOLUTION
  //
  //  Compute width of image box, correct even if image box rotated.
  //
  pNorm v01(p0,p1);
  pNorm v03(p0,p3);
  //
  const int box_wd = v01.magnitude;
  const int box_ht = v03.magnitude;
  //
  // Comment out previous code for pedagogical reasons.
  //
  // float box_wd = p1.x - p0.x;
  // float box_ht = p3.y - p0.y;

  // Image pixel array, and width and height, in pixels.
  //
  const auto& img_pixels = hw01_data.img_pixels;
  const int img_wd = hw01_data.img_px_width;
  const int img_ht = hw01_data.img_px_height;

  // Display Options
  //
  const float zoom [[maybe_unused]] = hw01_data.zoom;
  const int wrap_mode = hw01_data.wrap_mode;

  //  float img_scale_y = img_ht / box_ht;

  /// SOLUTION -- Problems 1 and 2
  //
  // Problem 2: Adjust img_scale_x based on zoom.
  //
  float img_scale_x = float(img_wd) / ( box_wd * zoom );
  //
  // Problem 1: Make y scale same as x scale to preserve aspect ratio.
  //
  const float img_scale_y = img_scale_x;


  // Write img_pixels to Frame Buffer at Image Box (p0,p1,p2,p3)
  //
  for ( int y=0; y<box_ht; y++ )
    for ( int x=0; x<box_wd; x++ )
      {
        // Compute x and y frame buffer coordinates.
        //
        // Original Code.
        // int fb_x = p0.x + x;
        // int fb_y = p0.y + y;
        //
        /// SOLUTION -- Problem 4
        //
        // Vector v01 points from p0 to p1 and its length is 1.
        // Vector v03 points from p0 to p3 and its length is 1.
        //
        // Variables x and y give the location of the frame buffer pixel
        // relative to p0. With no rotation we can compute fb_x by
        // just adding: fb_x = p0.x + x.
        //
        // But with rotation we need to use v01 *and* v03 to compute
        // fb_x:
        //
        int fb_x = p0.x + x * v01.x + y * v03.x;
        //
        // Similarly for fb_y:
        //
        int fb_y = p0.y + x * v01.y + y * v03.y;
        //
        // Some tried solving the problem by computing the angle by
        // which the image box is rotated, then using cosine and sine
        // of that angle to compute the rotated frame buffer
        // coordinates.  That's not necessary because cosine and sine
        // are just ratios of lengths of right triangle sides that we
        // already know:
        // Note: v01.x = cos( theta ) = ( p1.x - p0.x ) / box_wd
        //  where theta = arctan2( p1.y-p0.y, p1.x-p0.x ).
        // Notice that there is no need to compute theta since v01.x is
        // exactly what we need.

        // Compute preliminary image pixel coordinates.
        //
        int img_x_raw = x * img_scale_x;
        int img_y_raw = y * img_scale_y;
        //
        // These will be used to compute the pixel coordinates that
        // take the wrap mode into account.

        int img_x, img_y;

        // Adjust img_x and img_y based on wrap mode.
        //
        switch ( wrap_mode ) {
          /// SOLUTION -- Problem 3
        case WM_Repeat:
          // For repeat, just use the modulo operator.
          img_x = img_x_raw % img_wd;
          img_y = img_y_raw % img_ht;
          break;
        case WM_Mirror:
          {
            // For the mirror mode we need to know whether to apply mirroring
            // to that axis. So first we compute how many tiles are to
            // the left of img_x_raw, nx:
            //
            int nx = img_x_raw / img_wd;
            //
            // If there are an even number of tiles (including 0) we don't
            // mirror, otherwise we do.
            //
            int xm = img_x_raw % img_wd;
            img_x =  nx % 2  ?  img_wd - xm - 1  :  xm;
            //                  Mirror---------     Don't Mirror

            // Do the same for the y axis.
            int ny = img_y_raw / img_ht;
            int ym = img_y_raw % img_ht;
            img_y = ny % 2 ? img_ht - ym - 1 : ym;
            break;
          }
        case WM_Transparent:
          if ( img_x_raw >= img_wd || img_y_raw >= img_ht ) continue;
          img_x = img_x_raw;  img_y = img_y_raw;
          break;
        default:
          assert( false ); // Check for an unexpected wrap mode.
        }

        // Check whether image pixel coordinates are valid ..
        //
        const size_t img_idx = img_y * img_wd + img_x;
        const bool out_of_range = img_idx >= img_pixels.size();
        //
        // .. and read pixel if they are valid, otherwise use color red.
        //
        uint32_t img_pixel = out_of_range ? color_red : img_pixels[ img_idx ];
        //
        // Finally, write image pixel into frame buffer.
        //
        fb[ fb_y * win_width + fb_x ] = img_pixel;
      }


  /// Homework 1: No need to modify code below.
  //

  // Draw Lines Around Image Box
  //
  line_draw(fb, img_box[0], img_box[1], color_blue );
  line_draw(fb, img_box[1], img_box[2], color_green );
  line_draw(fb, img_box[2], img_box[3], color_red );
  line_draw(fb, img_box[0], img_box[3], 0xffffff );

  // Draw Image Box Corners
  //
  for ( auto& c: hw01_data.img_box )
    {
      int hwid = 10;
      pVect diag(hwid,hwid,0);
      pCoor ll = c - diag, ur = c + diag;
      bool inside = fb.mouse_x >= ll.x && fb.mouse_x <= ur.x
        && fb.mouse_y >= ll.y && fb.mouse_y <= ur.y;
      if ( inside && fb.mouse_button_left_state == GLUT_DOWN )
        hw01_data.img_box_dragging = distance(hw01_data.img_box,&c);
      uint32_t color = inside ? 0xff0000 : 0xffffff;
      aa_rectangle_draw(fb, ll, ur, color );
    }

}


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 ) )
    {
      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
    {
      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 )
{
  const int win_width = fb.width_get(), win_height = 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.

  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
aa_rectangle_draw
( pFrame_Buffer& fb, iCoor p00, iCoor p11, uint32_t color )
{
  iVect diag = p11 - p00;
  aa_rectangle_draw(fb, p00, diag.x, diag.y, color);
}


void
image_box_drag(pFrame_Buffer& fb, HW01_Data& hw01_data)
{
  // Coordinates of mouse pointer.
  //
  const pCoor mouse(fb.mouse_x,fb.mouse_y,0);

  auto& img_box = hw01_data.img_box;
  auto& drag_idx = hw01_data.img_box_dragging;

  // Turn off mouse dragging if button up, don't bother checking if it's on.
  if ( fb.mouse_button_left_state == GLUT_UP && drag_idx != 4 ) drag_idx = 4;

  const bool rot = drag_idx & 1;

  if ( drag_idx < 4 )
    {
      const int nbr_x_idx = (drag_idx+1) & 3, nbr_y_idx = (drag_idx+3) & 3;
      const int opp_idx = (drag_idx+2) & 3;
      pNorm pox_oldn(img_box[opp_idx],img_box[nbr_x_idx]);
      pNorm poy_oldn(img_box[opp_idx],img_box[nbr_y_idx]);
      pNorm doldn(img_box[opp_idx],img_box[drag_idx]);
      pVect dnew(img_box[opp_idx],mouse);
      if ( rot )
        {
          if ( pox_oldn.magnitude > 40 && poy_oldn.magnitude > 40 )
            {
              float costh = dot(dnew,doldn), sinth = cross(dnew,doldn).z;
              pVect pox_new = pox_oldn.magnitude / doldn.magnitude
                * (  costh * pox_oldn + sinth * poy_oldn );
              pVect poy_new = poy_oldn.magnitude / doldn.magnitude
                * ( -sinth * pox_oldn + costh * poy_oldn );
              img_box[drag_idx] = mouse;
              img_box[nbr_x_idx] = img_box[opp_idx] + pox_new;
              img_box[nbr_y_idx] = img_box[opp_idx] + poy_new;
            }
        }
      else
        {
          float lx = dot( pox_oldn, dnew ), ly = dot( poy_oldn, dnew );
          if ( fabs(lx) > 40 && fabs(ly) > 40 )
            {
              img_box[drag_idx] = mouse;
              img_box[nbr_x_idx] = img_box[opp_idx] + lx * pox_oldn;
              img_box[nbr_y_idx] = img_box[opp_idx] + ly * poy_oldn;
            }
        }
    }
}

void
image_box_reset(pFrame_Buffer& fb, HW01_Data& hw01_data)
{
  float img_aspect = float(hw01_data.img_px_width)/hw01_data.img_px_height;
  const int win_width = fb.width_get();
  const int win_height = fb.height_get();

  int box_width =
    img_aspect > 1 ? win_width / 2 : img_aspect * win_width / 2;
  int box_height =
    img_aspect < 1 ? win_height / 2 : box_width / img_aspect;

  pCoor img_center(win_width/2,win_height/2,0);
  pVect box_hw(box_width/2,0,0);
  pVect box_hh(0,box_height/2,0);

  hw01_data.img_box[0] = img_center - box_hw - box_hh;
  hw01_data.img_box[1] = img_center + box_hw - box_hh;
  hw01_data.img_box[2] = img_center + box_hw + box_hh;
  hw01_data.img_box[3] = img_center - box_hw + box_hh;
}

void
kbd_input_handle(pFrame_Buffer& fb, HW01_Data& hw01_data)
{
  if ( !hw01_data.inited )
    {
      hw01_data.inited = true;
      hw01_data.tryout1 = false;
      hw01_data.tryout2 = false;

      P_Image_Read image("pipelines_2x-crop-lab.png");
      hw01_data.img_pixels.reserve(image.size);
      hw01_data.img_px_height = image.height;
      hw01_data.img_px_width = image.width;
      auto sc = [&](uint32_t v){ return MaxRGB == 65535 ? v >> 8 : v; };
      // Copy image so that first pixel is lower-left.
      for ( int idx = image.size;  (idx-=image.width) >= 0; )
        for ( auto& pp: span(&image.pp[idx],image.width) )
          hw01_data.img_pixels.push_back
            ( sc(pp.red)<<16 | sc(pp.green)<<8 | sc(pp.blue) );

      image_box_reset(fb,hw01_data);
      hw01_data.img_box_dragging = 4;

      hw01_data.zoom = 1;
      hw01_data.wrap_mode = WM_Repeat;
     }

  switch ( fb.keyboard_key ) {
  case FB_KEY_RIGHT: case FB_KEY_LEFT:
    {
      break;
    }
  case FB_KEY_UP: case FB_KEY_DOWN:
    {
      break;
    }

  case 'R': image_box_reset(fb,hw01_data); break;

  case 'y':
    hw01_data.tryout1 = !hw01_data.tryout1; break;
  case 'Y':
    hw01_data.tryout2 = !hw01_data.tryout2; break;

  case 'w': case 'W': hw01_data.wrap_mode++;
    if ( hw01_data.wrap_mode == WM_ENUM_SIZE ) hw01_data.wrap_mode = 0;
    break;

  case 'u': hw01_data.zoom /= 1.1; break;
  case 'U': hw01_data.zoom /= 1.5; break;
  case 'z': hw01_data.zoom *= 1.1; break;
  case 'Z': hw01_data.zoom *= 1.5; break;

  default:
    if ( false && fb.keyboard_key ) printf("Unknown key, %d\n",fb.keyboard_key);
    break;
  };

  fb.fbprintf("Zoom: %.2f ('z','u')  Wrap Mode: %s ('w')  "
              "Tryout 1,2:  %s,%s ('y','Y')\n",
              hw01_data.zoom, wrap_mode_str[hw01_data.wrap_mode],
              hw01_data.tryout1 ? "TRUE " : "FALSE",
              hw01_data.tryout2 ? "TRUE " : "FALSE");

}

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