/// LSU EE 4702-1 (Fall 2013), GPU Programming
//
 /// Homework 2 - SOLUTION
 //

 /// Instructions
 //
 //  Read the assignment: http://www.ece.lsu.edu/koppel/gpup/2013/hw02.pdf


// $Id:$

/// Purpose
//
//   Demonstrate simulation of string modeled as point masses and springs


/// What Code Does

// Simulates a string bouncing over a platform. The string is modeled
// as point masses connected by springs with a long relaxed
// length. The platform consists of tiles, some are purple-tinted
// mirrors (showing a reflection of the ball), the others show the
// course syllabus.


///  Keyboard Commands
 //
 /// Object (Eye, Light, Ball) Location or Push
 //   Arrows, Page Up, Page Down
 //        Move object or push ball, depending on mode.
 //        With shift key pressed, motion is 5x faster.
 //   'e': Move eye.
 //   'l': Move light.
 //   'b': Move head (first) ball. (Change position but not velocity.)
 //   'B': Push head ball. (Add velocity.)
 //
 /// Eye Direction
 //   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.

 /// Simulation Options
 //  (Also see variables below.)
 //
 //  'v'    Switch between using "proba" and "probb" version of cone.
 //  '1'    Set up scene 1.
 //  '2'    Set up scene 2.
 //  'p'    Pause simulation. (Press again to resume.)
 //  ' '    (Space bar.) Advance simulation by 1/30 second.
 //  'S- '  (Shift-space bar.) Advance simulation by one time step.
 //  'k'    Freeze position of first ball. (Press again to release.)
 //  't'    Freeze position of last ball. (Press again to release.)
 //  's'    Stop ball.
 //  'S'    Freeze ball. (Set velocity of all vertices to zero.)
 //  'g'    Turn gravity on and off.
 //  '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 Level of Detail - Set level of detail to be used for cone. (opt_lod)
 //  VAR Spring Constant - Set spring constant.
 //  VAR Air Resistance - Set air resistance.
 //  VAR Light Intensity - The light intensity.
 //  VAR Gravity - Gravitational acceleration. (Turn on/off using 'g'.)


#define GL_GLEXT_PROTOTYPES
#define GLX_GLXEXT_PROTOTYPES

#include <GL/gl.h>
#include <GL/glext.h>
#include <GL/glx.h>
#include <GL/glxext.h>
#include <GL/glu.h>
#include <GL/freeglut.h>

#include <gp/util.h>
#include <gp/glextfuncs.h>
#include <gp/coord.h>
#include <gp/shader.h>
#include <gp/pstring.h>
#include <gp/misc.h>
#include <gp/gl-buffer.h>
#include <gp/texture-util.h>

#include "shapes.h"

int opt_lod;
bool opt_use_buffer_objects;

class Cone {
public:
  Cone(){
    apex_radius = 0.1; 
    dont_set_color = true;

    bo_lod = -1;  // Set an invalid level of detail.
    buffer_obj_coords = 0;
    buffer_obj_norms = 0;
  };
  void render_shadow_volume(pCoor base, float radius, pVect to_apex){}
  void render(pCoor base, float radius, pVect to_apex)
    {
      if ( opt_use_buffer_objects )
        render_probb(base,radius,to_apex);
      else
        render_proba(base,radius,to_apex);
    }
  void render_proba(pCoor base, float radius, pVect to_apex)
  {
    /// SOLUTION:  opt_lod times as many sides.
    const int sides = 10 * opt_lod;
    const double delta_theta = 2 * M_PI / sides;
    const double base_radius = 1;
    const double apex_height = 1;
    const double alpha = atan2(apex_height,base_radius-apex_radius);
    const double vec_z = sin(alpha);
    const float to_height = to_apex.mag();

    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();

    pVect from_apex(0,0,1);
    pNorm rn(from_apex,to_apex);
    const float rot_angle = pangle(from_apex,to_apex);
    pMatrix_Translate trans_transl(base);
    pMatrix_Rotation trans_rot(rn,rot_angle);
    pMatrix_Scale trans_scale(radius); 
    trans_scale.rc(2,2) = to_height;
    pMatrix xform = trans_transl * trans_rot * trans_scale;

    glMultTransposeMatrixf(xform.a);

    if ( !dont_set_color ) glColor3fv(color);


    /// SOLUTION
    //
    //  Draw multiple (opt_lod) quad strips.
    //
    const float delta_rad = ( apex_radius - base_radius ) / opt_lod;
    const float delta_height = apex_height / opt_lod;

    for ( int j=0; j<opt_lod; j++ )
      {
        // Scale radius and height.
        //
        const float r0 = base_radius + delta_rad * j;
        const float r1 = r0 + delta_rad;
        const float h0 = delta_height * j;
        const float h1 = h0 + delta_height;

        glBegin(GL_QUAD_STRIP);
        for ( int i=0; i<=sides; i++ )
          {
            const double theta = delta_theta * i;
            const double cos_t = cos(theta);
            const double sin_t = sin(theta);
            glNormal3f( cos_t, sin_t, vec_z );
            glVertex3f( r1 * cos_t, r1 * sin_t, h1);
            glVertex3f( r0 * cos_t, r0 * sin_t, h0);
          }
        glEnd();
      }

    glPopMatrix();
  }

  void render_probb(pCoor base, float radius, pVect to_apex)
    {
      /// SOLUTION

      // First, code updates the buffer objects if the lod needed
      // does not match the lod of the data stored in the buffer
      // objects.
      //
      // Then the code renders the cone.


      /// Update Buffer Object
      //
      if ( opt_lod != bo_lod )  // If needed lod != stored lod.
        {
          bo_lod = opt_lod;

          // Self-sizing arrays to store coordinates and normals.
          //
          PStack<float> coords;
          PStack<float> norms;

          const int sides = 10 * opt_lod;
          const double delta_theta = 2 * M_PI / sides;
          const double base_radius = 1;
          const double apex_height = 1;
          const double alpha = atan2(apex_height,base_radius-apex_radius);
          const double vec_z = sin(alpha);

          const float delta_rad = ( apex_radius - base_radius ) / opt_lod;
          const float delta_height = apex_height / opt_lod;

          for ( int j=0; j<opt_lod; j++ )
            {
              const float r0 = base_radius + delta_rad * j;
              const float r1 = r0 + delta_rad;
              const float h0 = delta_height * j;
              const float h1 = h0 + delta_height;

              for ( int i=0; i<=sides; i++ )
                {
                  const double theta = delta_theta * i;
                  const double cos_t = cos(theta);
                  const double sin_t = sin(theta);

                  // Add normals and coordinates to self-sizing arrays.
                  //
                  norms += cos_t;  norms += sin_t;  norms += vec_z;
                  norms += cos_t;  norms += sin_t;  norms += vec_z;
                  coords += r1 * cos_t; coords += r1 * sin_t;  coords += h1;
                  coords += r0 * cos_t; coords += r0 * sin_t;  coords += h0;
                }
            }

          // Save number of coordinates. (Needed by glDrawArrays)
          num_coords = coords.occ() / 3;

          // Generate buffer object names if we need them.
          //
          if ( !buffer_obj_coords )
            {
              glGenBuffers(1,&buffer_obj_coords);
              glGenBuffers(1,&buffer_obj_norms);
            }

          // Copy coordinates to buffer object.
          //
          glBindBuffer(GL_ARRAY_BUFFER, buffer_obj_coords);
          glBufferData
            (GL_ARRAY_BUFFER,          
             coords.occ() * sizeof(float),
             coords.get_storage(),
             GL_STATIC_DRAW);

          // Copy normals to buffer object.
          //
          glBindBuffer(GL_ARRAY_BUFFER, buffer_obj_norms);
          glBufferData
            (GL_ARRAY_BUFFER,          
             norms.occ() * sizeof(float),
             norms.get_storage(),
             GL_STATIC_DRAW);
        }


      // Compute transform so generic cone matches the cone that was
      // requested.
      //
      const float to_height = to_apex.mag();
      pVect from_apex(0,0,1);
      pNorm rn(from_apex,to_apex);
      const float rot_angle = pangle(from_apex,to_apex);
      pMatrix_Translate trans_transl(base);
      pMatrix_Rotation trans_rot(rn,rot_angle);
      pMatrix_Scale trans_scale(radius); 
      trans_scale.rc(2,2) = to_height;
      pMatrix xform = trans_transl * trans_rot * trans_scale;

      // Specify our transformation to OpenGL.
      //
      glMatrixMode(GL_MODELVIEW);
      glPushMatrix();
      glMultTransposeMatrixf(xform.a);

      if ( !dont_set_color ) glColor3fv(color);

      // Tell OpenGL to get coordinates and normals from buffer objects.
      //
      glBindBuffer(GL_ARRAY_BUFFER, buffer_obj_coords);
      glVertexPointer( 3, GL_FLOAT, 0, NULL);
      glEnableClientState(GL_VERTEX_ARRAY);

      glBindBuffer(GL_ARRAY_BUFFER, buffer_obj_norms);
      glNormalPointer(GL_FLOAT,0,NULL);
      glEnableClientState(GL_NORMAL_ARRAY);

      // Draw the cones. (There will be minor flaws since 1 strip used.)
      glDrawArrays(GL_QUAD_STRIP,0,num_coords);

      glBindBuffer(GL_ARRAY_BUFFER,0);
      glDisableClientState(GL_NORMAL_ARRAY);
      glDisableClientState(GL_VERTEX_ARRAY);

      glPopMatrix();
    }

  void set_color(const pColor &c) { color = c;  dont_set_color = false; }

  bool dont_set_color;
  pColor color;
  pCoor light_pos;
  double apex_radius;

  GLuint buffer_obj_coords, buffer_obj_norms;
  int bo_lod;
  int num_coords;
};


///
/// Main Data Structures
///
//
// class World: All data about scene.


class World;


// Object Holding Ball State
//
class Ball {
public:
  pCoor position;
  pVect velocity;
  float mass, mass_inv;
  float radius;
  bool contact;
  void push(pVect amt);
  void translate(pVect amt);
  void stop();
  void freeze();
};

#include "hw2-graphics.cc"


void
World::init()
{
  opt_lod = 1;
  variable_control.insert(opt_lod,"Level of Detail",1,1);

  opt_use_buffer_objects = false;

  chain_length = 10;
  balls = new Ball[chain_length];
 
  distance_relaxed = 25 / chain_length;
  opt_spring_constant = 1000;
  variable_control.insert(opt_spring_constant,"Spring Constant");

  opt_gravity_accel = 9.8;
  opt_gravity = true;
  gravity_accel = pVect(0,-opt_gravity_accel,0);
  variable_control.insert(opt_gravity_accel,"Gravity");

  opt_air_resistance = 0.001;
  variable_control.insert(opt_air_resistance,"Air Resistance");  

  world_time = 0;
  time_step_count = 0;
  last_frame_wall_time = time_wall_fp();
  frame_timer.work_unit_set("Steps / s");

  init_graphics();

  light_location = pCoor(30.8,12.8,-9.9);

  cone_fixed.set_color(color_chocolate);
  cone_fixed_position = pCoor(28.5,0,-8.4);
  cone_fixed_radius = 3;
  cone_fixed_height = 40;

  ball_setup_2();
}

///
/// Physical Simulation Code
///

 /// Initialize Simulation
//
void
World::ball_setup_1()
{
  // Set initial position to a visibly interesting point.
  //
  pCoor next_pos(12.5,0.1,-13.7);

  for ( int i=0; i<chain_length; i++ )
    {
      // Put the first ball on top because that one can be moved and locked.
      //
      Ball* const ball = &balls[chain_length-i-1];
      ball->position = next_pos;
      ball->velocity = pVect(0,0,0);
      ball->radius = 0.5;
      ball->mass = 4/3.0 * M_PI * pow(ball->radius,3);
      ball->contact = false;
      next_pos += pVect(0.1,distance_relaxed,0);
    }
}

void
World::ball_setup_2()
{
  // Arrange and size balls to form a pendulum.
  //
  pCoor next_pos(13.4,21.8,-9.2);

  for ( int i=0; i<chain_length; i++ )
    {
      // Put the first ball on top because that one can be moved and locked.
      //
      Ball* const ball = &balls[i];
      ball->position = next_pos;
      ball->velocity = pVect(0,0,0);
      ball->radius = i == chain_length - 1 ? 1 : 0.5;
      ball->mass = 4/3.0 * M_PI * pow(ball->radius,3);
      ball->contact = false;
      next_pos += pVect(distance_relaxed,0,0);
    }

  opt_head_lock = true;
}

void
World::ball_setup_3()
{
}

void
World::ball_setup_4()
{
}

void
World::ball_setup_5()
{
}


 /// Advance Simulation State by delta_t Seconds
//
void
World::time_step_cpu(double delta_t)
{
  time_step_count++;

  //
  /// Compute force and update velocity of each ball.
  //
  for ( int i=0; i<chain_length; i++ )
    {
      Ball* const ball = &balls[i];

      // Skip locked balls.
      //
      if ( opt_head_lock && i == 0 || opt_tail_lock && i == chain_length - 1 )
        {
          ball->velocity = pVect(0,0,0);
          continue;
        }

      pVect force(0,0,0);

      // Gravitational Force
      //
      force += ball->mass * gravity_accel;

      // Spring Force from Neighbor Balls
      //
      for ( int n_idx = i-1; n_idx <= i+1; n_idx += 2 )
        {
          if ( n_idx < 0 ) continue;
          if ( n_idx == chain_length ) break;

          Ball* const neighbor_ball = &balls[n_idx];

          // Construct a normalized (Unit) Vector from ball to neighbor.
          //
          pNorm ball_to_neighbor(ball->position,neighbor_ball->position);

          // Compute the speed of ball towards neighbor_ball.
          //
          pVect delta_v = neighbor_ball->velocity - ball->velocity;
          float delta_s = dot( delta_v, ball_to_neighbor );

          // Compute by how much the spring is stretched (positive value)
          // or compressed (negative value).
          //
          const float spring_stretch =
            ball_to_neighbor.magnitude - distance_relaxed;

          // Determine whether spring is gaining energy (whether its length
          // is getting further from its relaxed length).
          //
          const bool gaining_e = ( delta_s > 0.0 ) == ( spring_stretch > 0 );

          // Use a smaller spring constant when spring is loosing energy,
          // a quick and dirty way of simulating energy loss due to spring
          // friction.
          //
          const float spring_constant =
            gaining_e ? opt_spring_constant : opt_spring_constant * 0.7;

          force += spring_constant * spring_stretch * ball_to_neighbor;
        }

      // Update Velocity
      //
      // This code assumes that force on ball is constant over time
      // step. This is clearly wrong when balls are moving with
      // respect to each other because the springs are changing
      // length. This inaccuracy will make the simulation unstable
      // when spring constant is large for the time step.
      //
      ball->velocity += ( force / ball->mass ) * delta_t;

      // Air Resistance
      //
      const double fs = pow(1+opt_air_resistance,-delta_t);
      ball->velocity *= fs;
    }

  ///
  /// Update Position of Each Ball
  ///

  for ( int i=0; i<chain_length; i++ )
    {
      Ball* const ball = &balls[i];

      // Update Position
      //
      // Assume that velocity is constant.
      //
      ball->position += ball->velocity * delta_t;

      // Possible Collision with Platform
      //

      // Skip if collision impossible.
      //
      if ( !platform_collision_possible(ball->position) ) continue;
      if ( ball->position.y >= 0 ) continue;

      // Snap ball position to surface.
      //
      ball->position.y = 0;

      // Reflect y (vertical) component of velocity, with a reduction
      // due to energy lost in the collision.
      //
      if ( ball->velocity.y < 0 )
        ball->velocity.y = - 0.9 * ball->velocity.y;
    }
}

bool
World::platform_collision_possible(pCoor pos)
{
  // Assuming no motion in x or z axes.
  //
  return pos.x >= platform_xmin && pos.x <= platform_xmax
    && pos.z >= platform_zmin && pos.z <= platform_zmax;
}

 /// External Modifications to State
//
//   These allow the user to play with state while simulation
//   running.

// Move the ball.
//
void Ball::translate(pVect amt) {position += amt;}

// Add velocity to the ball.
//
void Ball::push(pVect amt) {velocity += amt;}

// Set the velocity to zero.
//
void Ball::stop() {velocity = pVect(0,0,0); }

// Set the velocity and rotation (not yet supported) to zero.
//
void Ball::freeze() {velocity = pVect(0,0,0); }



void World::balls_translate(pVect amt,int b){balls[b].translate(amt);}
void World::balls_push(pVect amt,int b){balls[b].push(amt);}
void World::balls_translate(pVect amt)
{ for(int i=0;i<chain_length;i++)balls[i].translate(amt);}
void World::balls_push(pVect amt)
{ for(int i=0;i<chain_length;i++)balls[i].push(amt);}
void World::balls_stop()
{ for(int i=0;i<chain_length;i++)balls[i].stop();}
void World::balls_freeze(){balls_stop();}


void
World::frame_callback()
{
  // This routine called whenever window needs to be updated.

  const double time_now = time_wall_fp();

  if ( !opt_pause || opt_single_frame || opt_single_time_step )
    {
      /// Advance simulation state.

      // Amount of time since the user saw the last frame.
      //
      const double wall_delta_t = time_now - last_frame_wall_time;

      const double time_step_duration = 0.0001;

      // Compute amount by which to advance simulation state for this frame.
      //
      const double duration =
        opt_single_time_step ? time_step_duration :
        opt_single_frame ? 1/30.0 : 
        wall_delta_t;

      const double world_time_target = world_time + duration;

      while ( world_time < world_time_target )
        {
          time_step_cpu(time_step_duration);
          world_time += time_step_duration;
        }

      // Reset these, just in case they were set.
      //
      opt_single_frame = opt_single_time_step = false;
    }

  last_frame_wall_time = time_now;
  render();
}




int
main(int argv, char **argc)
{
  pOpenGL_Helper popengl_helper(argv,argc);
  World world(popengl_helper);

  popengl_helper.rate_set(30);
  popengl_helper.display_cb_set(world.frame_callback_w,&world);
}