/// LSU EE 4702-1 (Fall 2021), GPU Programming
//
 /// Simple Demo of Dynamic Simulation, Graphics Code

 // This file includes graphics code needed by the main demo file,
 // demo-01-simple.cc.  The code in this file does not need to
 // be understood early in the semester.

// Include files provided for this course.
//
#define MAIN_INCLUDE
#include <vhelper.h>
#include <vstroke.h>
#include <gp/coord.h>
#include <gp/pstring.h>
#include <gp/misc.h>

#include <vutil-pipeline.h>
#include <vutil-texture.h>

#include "shapes.h"



class World {
public:
  World(pVulkan_Helper &fb)
    :vh(fb), ff_state(fb.qs),
     frame_timer(vh.frame_timer),
     shapes(ff_state), sphere(ff_state), cone(ff_state)
  {init();}
  World() = delete;
  void init();
  void init_graphics();
  void run();
  void frame_callback();
  void render(vk::CommandBuffer& cb);
  void cb_keyboard();
  void modelview_update();
  void shadow_update();
  void shadow_transform_create(pMatrix& m, pCoor light);

  pVulkan_Helper& vh;
  VFixed_Function_State_Manager ff_state;
  pFrame_Timer& frame_timer;
  Shapes shapes;

  pVariable_Control variable_control;
  double world_time;
  double last_frame_wall_time;
  int time_step_count;
  float opt_gravity_accel;      // Value chosen by user.
  pVect gravity_accel;          // Set to zero when opt_gravity is false;
  bool opt_gravity;
  bool opt_time_step_alt;
  bool opt_tryout1, opt_tryout2;

  // Tiled platform for bal.
  //
  float platform_xmin, platform_xmax, platform_zmin, platform_zmax;
  float platform_pi_xwidth_inv;
  VVertex_Buffer_Set bset_platform_tex, bset_platform_mir;
  VPipeline pipe_platform_mir, pipe_platform_tex;

  vk::Sampler sampler;
  VTexture texid_syl;
  VTexture texid_emacs;

  bool opt_platform_texture;
  bool opt_platform_flat;
  void platform_update();
  bool platform_collision_possible(pCoor pos);

  pCoor light_location;
  float opt_light_intensity;
  enum { MI_Eye, MI_Light, MI_Ball, MI_Ball_V, MI_COUNT } opt_move_item;
  bool opt_pause;
  bool opt_single_frame;      // Simulate for one frame.
  bool opt_single_time_step;  // Simulate for one time step.

  pCoor eye_location;
  pVect eye_direction;

  VTransform transform;
  VBufferV<Uni_Lighting> uni_light, uni_light_ambient, uni_light_all;
  VBufferV<Uni_Lighting> *puni_light_curr;

  pMatrix modelview;
  pMatrix modelview_shadow;
  pMatrix transform_mirror;

  void time_step_cpu_v0(double);

  Ball ball;
  Sphere sphere;
  Cone cone;

  float guide_distance;
};



void
World::init_graphics()
{
  vh.init();
  vh.opt_record_every_time = true;
  vh.display_cb_set([&](){frame_callback();});
  vh.cbs_cmd_record.push_back( [&](vk::CommandBuffer& cb){ render(cb); });

  ///
  /// Graphical Model Initialization
  ///

  opt_platform_flat = true;
  opt_light_intensity = 100.2;
  light_location = pCoor(platform_xmax,platform_xmax,platform_zmin);
  pCoor platform_center
    ( ( platform_xmin + platform_xmax ) / 2,
      platform_xmax / 2,
      ( platform_zmin + platform_zmax ) / 2 );
  eye_location = pCoor( platform_center.x, platform_center.y, platform_zmin );
  pCoor ball_pos_half(ball.position); ball_pos_half.y *= 0.5;
  eye_direction = pNorm(ball_pos_half - eye_location);

  uni_light.init(vh.qs,vk::BufferUsageFlagBits::eUniformBuffer);
  uni_light_all.init(vh.qs,vk::BufferUsageFlagBits::eUniformBuffer);
  uni_light_ambient.init(vh.qs,vk::BufferUsageFlagBits::eUniformBuffer);
  cone.ppuni_light = &puni_light_curr;
  cone.transform = &transform;

  opt_platform_texture = true;
  texid_syl.init( vh.qs, P_Image_Read("gpup.png",255) );
  texid_emacs.init( vh.qs, P_Image_Read("mult.png",-1) );

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

  guide_distance = 0.5;
  variable_control.insert_linear(guide_distance,"Guide Distance",.05);

  time_step_count = 0;

  // Common to all textures.
  //
  sampler = vh.qs.dev.createSampler
    ( { {},
        vk::Filter::eLinear, vk::Filter::eLinear,
        vk::SamplerMipmapMode::eLinear,
        // Also: eRepeat, eMirroredRepeat, eClampToEdge, etc.
        vk::SamplerAddressMode::eRepeat,
        vk::SamplerAddressMode::eRepeat,
        vk::SamplerAddressMode::eRepeat,
        0.0f,
        false, // anisotropyEnable,
        16.0f,
        false,
        vk::CompareOp::eNever,
        0.0f, VK_LOD_CLAMP_NONE,  // min and max LOD
        vk::BorderColor::eFloatOpaqueBlack } );


  opt_move_item = MI_Eye;
  opt_pause = false;
  opt_single_frame = false;
  opt_single_time_step = false;
  opt_time_step_alt = false;

  frame_timer.work_unit_set("Steps / s");
  sphere.transform = &transform;
  sphere.ppuni_light = &puni_light_curr;
  sphere.init(40);
  sphere.center = ball.position;

  platform_update();
  modelview_update();
}

void
World::run()
{
  vh.message_loop_spin();

  uni_light.destroy();
  uni_light_all.destroy();
  uni_light_ambient.destroy();
  sphere.destroy();
  pipe_platform_tex.destroy();
  pipe_platform_mir.destroy();
  bset_platform_mir.destroy();
  bset_platform_tex.destroy();
  texid_syl.destroy();
  texid_emacs.destroy();
  vh.dev.destroySampler(sampler);
  shapes.destroy();
  cone.destroy();

  vh.finish();
}



void
World::platform_update()
{
  const float tile_count = 19;
  const float ep = 1.00001;
  const float platform_delta_x = platform_xmax - platform_xmin;
  const float zdelta = ( platform_zmax - platform_zmin ) / tile_count * ep;

  const float trmin = 0.05;
  const float trmax = 0.7;
  const float tsmin = 0;
  const float tsmax = 0.4;
  pTCoor t00(trmin,tsmin), t01(trmin,tsmax), t10(trmax,tsmin), t11(trmax,tsmax);

  pColor white(1,1,1);
  const pColor lsu_spirit_purple(0x580da6);

  if ( !pipe_platform_mir )
    pipe_platform_mir
      .init( ff_state )
      .color_uniform_set( lsu_spirit_purple )
      .follow_uni_light( &puni_light_curr )
      .topology_set( vk::PrimitiveTopology::eTriangleList )
      .create( vh.qs.render_pass );
  bset_platform_mir.reset( pipe_platform_mir ).tolerate_tcoords();

  if ( !pipe_platform_tex )
    pipe_platform_tex
      .init( ff_state )
      .color_uniform_set(white)
      .follow_uni_light( &puni_light_curr )
      .topology_set( vk::PrimitiveTopology::eTriangleList )
      .use_texture( sampler, texid_syl )
      .create( vh.qs.render_pass );
  bset_platform_tex.reset( pipe_platform_tex );

  bool even = true;
  const bool curved = false;
  platform_pi_xwidth_inv = M_PI / platform_delta_x;

  const double delta_theta = M_PI / tile_count;
  const float delta_x = ( platform_xmax - platform_xmin ) / tile_count;
  const float platform_xmid = 0.5 * ( platform_xmax + platform_xmin );
  const float platform_xhlf = 0.5 * platform_delta_x;
  const double platform_depth = 0;

  for ( int i = 0; i < tile_count; i++ )
    {
      const double theta0 = i * delta_theta;
      const double theta1 = theta0 + delta_theta;
      const float x0 = curved
        ? platform_xmid - platform_xhlf * cos(theta0)
        : platform_xmin + i * delta_x;
      const float x1 = curved
        ? platform_xmid - platform_xhlf * cos(theta1)
        : x0 + delta_x;
      const float y0 = -0.01 - platform_depth * sin(theta0);
      const float y1 = -0.01 - platform_depth * sin(theta1);
      pNorm normc0( platform_depth * cos(theta0), platform_xhlf*sin(theta0), 0);
      pNorm normc1( platform_depth * cos(theta1), platform_xhlf*sin(theta1), 0);
      pVect norm0 = curved ? normc0 : pVect(0,1,0);
      pVect norm1 = curved ? normc1 : pVect(0,1,0);
      for ( float z = platform_zmin; z < platform_zmax; z += zdelta )
        {
          auto& bset = even ? bset_platform_tex : bset_platform_mir;
          const float z1 = z+zdelta;
          pCoor p00(x0,y0,z), p01(x0,y0,z1), p10(x1,y1,z), p11(x1,y1,z1);

          bset << t11 << norm0 << p00;
          bset << t10 << norm0 << p01;
          bset << t00 << norm1 << p11;

          bset << t11 << norm0 << p00;
          bset << t00 << norm1 << p11;
          bset << t01 << norm1 << p10;

          even = !even;
        }
    }

  bset_platform_mir.to_dev();
  bset_platform_tex.to_dev();
}

void
World::modelview_update()
{
  pMatrix_Translate center_eye(-eye_location);
  pMatrix_Rotation rotate_eye(eye_direction,pVect(0,0,-1));
  modelview = rotate_eye * center_eye;
  shadow_update();
}

void
World::shadow_update()
{
  // These routines need to be made more general.
  pCoor platform_point(platform_xmin,0,platform_zmin);
  pVect platform_normal(0,1,0);
  shadow_transform_create(modelview_shadow,light_location);
  pCoor eye_loc_mirror(eye_location.x, -eye_location.y, eye_location.z);
  pMatrix reflect; reflect.set_identity(); reflect.rc(1,1) = -1;
  transform_mirror = modelview * reflect * invert(modelview);
}

void
World::shadow_transform_create(pMatrix& m, pCoor light_location)
{
  pVect platform_normal(0,1,0);
  pVect eye_normal(0,0,-1);
  pMatrix_Translate center_light(-light_location);
  pNorm axis(-platform_normal,eye_normal);
  const double angle = asin(axis.magnitude);
  pMatrix_Rotation rotate_platform(axis,angle);
  pMatrix frustum; frustum.set_zero();
  frustum.rc(0,0) = frustum.rc(1,1) = light_location.y;
  frustum.rc(3,2) = -1;
  pMatrix_Translate restore_z(0,0,-light_location.y);
  pMatrix step1 = rotate_platform * center_light;
  pMatrix to_platform = restore_z * frustum * rotate_platform * center_light;
  pMatrix_Rotation un_rotate_platform(axis,-angle);
  pMatrix_Translate un_center_light(light_location);
  pMatrix from_platform = un_center_light * un_rotate_platform;
  pMatrix project = from_platform * to_platform;
  modelview_shadow = modelview * from_platform * to_platform;

  // Compute coordinates to help with debugging.
  //
  pCoor test_pt(1.1,0,2.2);
  pCoor test_pt2(1.1,1,2.2);
  pCoor test_pt_a = step1 * test_pt;
  pCoor test_pt_b = to_platform * test_pt;  test_pt_b.homogenize();
  pCoor test_pt_pr = project * test_pt;  test_pt_pr.homogenize();
  pCoor test_pt2_pr = project * test_pt2;  test_pt2_pr.homogenize();

  assert( test_pt_a.v() != nullptr ); // Just to avoid unused var warnings.
}

void
World::render(vk::CommandBuffer& cb)
{
  // Reset these, just in case they were set.
  //
  // This code more properly belongs near the call of the time step routine,
  // but put it here to keep things simple in student-facing code.
  //
  opt_single_frame = opt_single_time_step = false;

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

  // Start a timer object used for tuning this code.
  //
  //  frame_timer.frame_start();

  /// Emit a Graphical Representation of Simulation State
  //

  // The "ball" is part of the simulated state, "sphere" is the
  // graphical object used to show it.
  //
  sphere.center = ball.position;

  // Understanding of the code below not required for introductory
  // lectures.

  // That said, much of the complexity of the code is to show
  // the ball shadow and reflection.

  const pColor white(0xffffff);
  const pColor gray(0x303030);
  const pColor lsu_business_purple(0x7f5ca2);
  const pColor lsu_spirit_purple(0x580da6);
  const pColor lsu_spirit_gold(0xf9b237);
  const pColor lsu_official_purple(0x2f0462);
  const pColor dark(0);

  const int win_width = vh.get_width();
  const int win_height = vh.get_height();
  const float aspect = float(win_width) / win_height;

  transform.at_start();
  transform.eye_from_global_set(modelview);
  pMatrix_Scale flip(1,-1,1);
  transform.clip_from_eye_set
    ( flip * pMatrix_Frustum(-.8,.8,-.8/aspect,.8/aspect,1,5000) );
  // Frustum: left, right, bottom, top, near, far

  vh.clearValues[0].color = vk::ClearColorValue( array{0, 0, 0, 1 } );
  vh.clearValues[1].depthStencil = vk::ClearDepthStencilValue( 1, 0 );

  auto& light_all = uni_light_all->cgl_LightSource[0];
  light_all.position = transform.eye_from_global * light_location;
  light_all.constantAttenuation = .3;
  light_all.linearAttenuation = 1;
  light_all.quadraticAttenuation = 0;


  pColor ambient_color(0x999999);

  uni_light_all->cgl_LightModel.ambient = ambient_color;
  light_all.diffuse = white * opt_light_intensity;
  light_all.ambient = dark;
  light_all.specular = white * opt_light_intensity;

  uni_light_ambient = uni_light_all.get_val();
  auto& light_amb = uni_light_ambient->cgl_LightSource[0];
  light_amb.diffuse = dark;
  light_amb.specular = dark;

  uni_light = uni_light_all.get_val();
  uni_light->cgl_LightModel.ambient = dark;

  uni_light.to_dev();
  uni_light_ambient.to_dev();
  uni_light_all.to_dev();

  puni_light_curr = &uni_light_all;


  const double time_now = time_wall_fp();
  const bool blink_visible = int64_t(time_now*3) & 1;
# define BLINK(txt,pad) ( blink_visible ? txt : pad )

  vh.fbprintf("%s\n",frame_timer.frame_rate_text_get());

  vh.fbprintf
    ("Time Step: %8d  World Time: %11.6f  %s\n",
     time_step_count, world_time,
     opt_pause ? BLINK("PAUSED, 'p' to unpause, SPC or S-SPC to step.","") :
     "Press 'p' to pause.");

  vh.fbprintf
    ("Tryout 1: %s  ('y')  Tryout 2: %s  ('Y')\n",
     opt_tryout1 ? BLINK("ON ","   ") : "OFF",
     opt_tryout2 ? BLINK("ON ","   ") : "OFF" );

  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
    ("Ball Pos  [%5.1f,%5.1f,%5.1f] Vel [%+5.1f,%+5.1f,%+5.1f]\n",
     ball.position.x,ball.position.y,ball.position.z,
     ball.velocity.x,ball.velocity.y,ball.velocity.z );

  pVariable_Control_Elt* const cvar = variable_control.current;
  vh.fbprintf("VAR %s = %.5f  (TAB or '`' to change, +/- to adjust)\n",
                      cvar->name,cvar->var[0]);

  vh.fbprintf("Time Step Routine: time_step_v%d\n", opt_time_step_alt);

  pCoor guide_loc =
    ball.position + ( eye_location - ball.position ) * guide_distance;

  if ( opt_platform_flat )
    {
      //
      // Render ball reflection.  (Will be blended with dark tiles.)
      //

      ff_state.push();

      // Write stencil at location of dark (mirrored) tiles.
      //
      vk::StencilOpState s_op_apply
        ( vk::StencilOp::eReplace, vk::StencilOp::eKeep,
          vk::StencilOp::eKeep, vk::CompareOp::eNever, 2, 2, 2 );
      // sfail, spass, dfail, compare;  cmask, wmask, ref

      ff_state.pipelineDepthStencilStateCreateInfo_get()
        .setStencilTestEnable(true)
        .setFront(s_op_apply).setBack(s_op_apply);

      assert( pipe_platform_mir );
      transform.use_global_for(pipe_platform_mir);
      pipe_platform_mir.record_draw(cb,bset_platform_mir);

      cb.nextSubpass(vk::SubpassContents::eInline); vh.qs.subpass++;


      // Prepare to write only stenciled locations.
      //
      vk::StencilOpState s_op_use
        ( vk::StencilOp::eKeep, vk::StencilOp::eKeep,
          vk::StencilOp::eKeep, vk::CompareOp::eEqual, 2, 2, 2 );
      // sfail, spass, dfail, compare;  cmask, wmask, ref

      ff_state.pipelineDepthStencilStateCreateInfo_get()
        .setFront(s_op_use).setBack(s_op_use);

      // Use a transform that reflects objects to other side of platform.
      //
      const pMatrix prev = transform.eye_from_global;
      const pMatrix_Scale reflect(1,-1,1);
      transform.eye_from_global = prev * reflect;

      // Reflected front face should still be treated as the front face.
      //
      ff_state.pipelineRasterizationStateCreateInfo_get()
        .setFrontFace(vk::FrontFace::eClockwise);

      sphere.render(cb);

      cone.set_color(pColor(1,0,0));
      cone.render(cb, guide_loc, 0.1, pVect(1,0,0));
      cone.set_color(pColor(0,1,0));
      cone.render(cb, guide_loc, 0.1, pVect(0,1,0));
      cone.set_color(pColor(0,0,1));
      cone.render(cb, guide_loc, 0.1, pVect(0,0,1));

      transform.eye_from_global = prev;

      // blend operation, blend factors, blend constants
      ff_state.pipelineColorBlendStateCreateInfo_get()
        .setBlendConstants( array<float,4>{.5,.5,.5,.5} );

      vk::ColorComponentFlags clr_all
        ( vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
          vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA );

      ff_state.pipelineColorBlendAttachmentState_get() =
        vk::PipelineColorBlendAttachmentState
        ( true,                   // blendEnable
          vk::BlendFactor::eConstantAlpha,  // srcColorBlendFactor
          vk::BlendFactor::eConstantAlpha,  // dstColorBlendFactor
          vk::BlendOp::eAdd,       // colorBlendOp
          vk::BlendFactor::eOne,   // srcAlphaBlendFactor
          vk::BlendFactor::eZero,  // dstAlphaBlendFactor
          vk::BlendOp::eAdd,       // alphaBlendOp
          clr_all      // colorWriteMask
          );

      pipe_platform_mir.record_draw(cb,bset_platform_mir);

      ff_state.pop();
    }


  /// Render Objects with Ambient Light
  //
  transform.use_global_for(pipe_platform_tex);
  puni_light_curr = &uni_light_ambient;

  cone.set_color(pColor(1,0,0));
  cone.render(cb, guide_loc, 0.1, pVect(1,0,0));
  cone.set_color(pColor(0,1,0));
  cone.render(cb, guide_loc, 0.1, pVect(0,1,0));
  cone.set_color(pColor(0,0,1));
  cone.render(cb, guide_loc, 0.1, pVect(0,0,1));

  sphere.color = lsu_spirit_gold;
  sphere.render(cb,ball.position);

  pCoor pos2 = ball.position + pVect(2,0,0);
  sphere.color = lsu_spirit_purple;
  sphere.render(cb,pos2);

  pipe_platform_tex.record_draw(cb,bset_platform_tex);

  /// Write Shadow Volumes
  //
  vk::ClearAttachment clr_att
    ( vk::ImageAspectFlagBits::eStencil, 0, vk::ClearDepthStencilValue(0,0) );
  vk::ClearRect clr_rect( vk::Rect2D({}, vh.s_extent), 0, 1 );
  cb.clearAttachments( clr_att, clr_rect );

  ff_state.push();

  vk::StencilOpState s_op_apply_f
    ( vk::StencilOp::eKeep, vk::StencilOp::eIncrementAndWrap,
      vk::StencilOp::eKeep, vk::CompareOp::eAlways, -1, -1, 0 );
  vk::StencilOpState s_op_apply_b
    ( vk::StencilOp::eKeep, vk::StencilOp::eDecrementAndWrap,
      vk::StencilOp::eKeep, vk::CompareOp::eAlways, -1, -1, 0 );
  // sfail, spass, dfail, compare; c-mask, w-mask, ref

  ff_state.pipelineDepthStencilStateCreateInfo_get()
    .setStencilTestEnable(true)
    .setDepthWriteEnable(false)
    .setFront(s_op_apply_f).setBack(s_op_apply_b);
  ff_state.pipelineColorBlendAttachmentState_get().setColorWriteMask( {} );

  cone.light_pos = sphere.light_pos = light_location;
  sphere.render_shadow_volume(cb,sphere.radius,ball.position);
  sphere.render_shadow_volume(cb,sphere.radius,pos2);
  cone.render_shadow_volume(cb, guide_loc, 0.1, pVect(1,0,0));
  cone.render_shadow_volume(cb, guide_loc, 0.1, pVect(0,1,0));
  cone.render_shadow_volume(cb, guide_loc, 0.1, pVect(0,0,1));


  /// Render Objects Again Except to Shadowed Areas

  cb.nextSubpass(vk::SubpassContents::eInline); vh.qs.subpass++;

  vk::StencilOpState s_op_apply_fb
    ( vk::StencilOp::eKeep, vk::StencilOp::eKeep,
      vk::StencilOp::eKeep, vk::CompareOp::eEqual, -1, -1, 0 );
  // sfail, spass, dfail, compare;  c-mask, w-mask, ref

  ff_state.pipelineDepthStencilStateCreateInfo_get()
    .setFront(s_op_apply_fb).setBack(s_op_apply_fb)
    .setDepthCompareOp(vk::CompareOp::eEqual);

  vk::ColorComponentFlags cmask_all
    ( vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
      vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA );

  ff_state.pipelineColorBlendAttachmentState_get() =
    vk::PipelineColorBlendAttachmentState
    ( true,                   // blendEnable
      vk::BlendFactor::eOne,  // srcColorBlendFactor
      vk::BlendFactor::eOne,  // dstColorBlendFactor
      vk::BlendOp::eAdd,       // colorBlendOp
      vk::BlendFactor::eOne,   // srcAlphaBlendFactor
      vk::BlendFactor::eOne,  // dstAlphaBlendFactor
      vk::BlendOp::eAdd,       // alphaBlendOp
      cmask_all      // colorWriteMask
      );

  puni_light_curr = &uni_light;

  pipe_platform_tex.record_draw(cb,bset_platform_tex);

  cone.set_color(pColor(1,0,0));
  cone.render(cb, guide_loc, 0.1, pVect(1,0,0));
  cone.set_color(pColor(0,1,0));
  cone.render(cb, guide_loc, 0.1, pVect(0,1,0));
  cone.set_color(pColor(0,0,1));
  cone.render(cb, guide_loc, 0.1, pVect(0,0,1));

  sphere.color = lsu_spirit_gold;
  sphere.render(cb,ball.position);

  sphere.color = lsu_spirit_purple;
  sphere.render(cb,pos2);

  ff_state.pop();


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


void
World::cb_keyboard()
{
  const int key = vh.keyboard_key_get();
  if ( !key ) return;
  pVect adjustment(0,0,0);
  pVect user_rot_axis(0,0,0);
  const bool shift = vh.keyboard_shift;
  const float move_amt = shift ? 2.0 : 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 'b': opt_move_item = MI_Ball; break;
  case 'B': opt_move_item = MI_Ball_V; break;
  case 'e': case 'E': opt_move_item = MI_Eye; break;
  case 'f': case 'F': opt_platform_flat = !opt_platform_flat; break;
  case 'g': case 'G': opt_gravity = !opt_gravity; break;
  case 'l': case 'L': opt_move_item = MI_Light; break;
  case 'n': case 'N': opt_platform_texture = !opt_platform_texture; break;
  case 'p': case 'P': opt_pause = !opt_pause; break;
  case 's': case 'S': ball.stop(); break;
  case 'v': case 'V': opt_time_step_alt = !opt_time_step_alt; break;
  case 'y': opt_tryout1 = !opt_tryout1; break;
  case 'Y': opt_tryout2 = !opt_tryout2; break;
  case ' ': 
    if ( shift ) opt_single_time_step = true; else opt_single_frame = true;
    opt_pause = true; 
    break;
  case FB_KEY_TAB:
    if ( !shift ) { 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;
  }

  gravity_accel.y = opt_gravity ? -opt_gravity_accel : 0;

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

  // 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_Ball: ball.translate(adjustment); break;
      case MI_Ball_V: ball.push(adjustment); break;
      case MI_Light: light_location += adjustment; break;
      case MI_Eye: eye_location += adjustment; break;
      default: break;
      }
      modelview_update();
    }

}