// sample problem discussed at the lecture 9/16/2004

#define GLUT_API_VERSION 4

#include <iostream>
#include <strstream>

#ifdef _WIN32
#include <windows.h>
#endif
#include <math.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glut.h>
#ifndef M_PI
#define M_PI            3.14159265358979323846
#endif 

#include "cvec2t.h"
#include "cvec3t.h"

typedef CVec2T<float> Vec2f;
typedef CVec3T<float> Vec3f;


namespace WindowParams {
  static int WindowWidth = 800;
  static int WindowHeight = 600;
  static int MainWindow; 
};

namespace Params { 
  const float WorldWidth = 1.33;
  const float WorldHeight = 1;
  const int TimerStep = 16; // millisec

  const Vec3f BackgroundColor(0.5,0.5,0.5);

  const Vec2f CourtLowerLeftCorner(-WorldWidth*0.5,-WorldHeight*0.4);  
  const Vec2f CourtSize(WorldWidth, WorldHeight*0.9); 

  const Vec2f BallInitPos(0,0); 
  const Vec2f BallInitVelocity(0.004*CourtSize.x(),0.003*CourtSize.y()); // in world units per frame 
  const Vec3f BallColor(1,1,1); 
  const float BallRadius = 0.02*WorldHeight;
  const int   BallNumSegments = 25;
  
  const Vec2f PaddleSize(0.01*CourtSize.y(),0.1*CourtSize.y()); // size defined relative to vertical court dimension
  const float PaddleLeftHPos = 0.1*CourtSize.x(); // relative displacement from sides
  const float PaddleRightHPos = 0.9*CourtSize.x();
  const Vec2f PaddleVelocity(0,0.04*CourtSize.y()); // velocity in world units per frame, 
  const Vec3f PaddleLeftColor(0,0,1); // blue
  const Vec3f PaddleRightColor(1,0,0); // red

  const unsigned char PaddleLeftKeyUp   = 'Q';
  const unsigned char PaddleLeftKeyDown = 'A';
  const unsigned char PaddleRightKeyUp   = 'P';
  const unsigned char PaddleRightKeyDown = 'L';


  const Vec2f InfoLowerLeftCorner(-WorldWidth*0.5,-WorldHeight*0.5);
  const Vec2f InfoSize(WorldWidth,0.1*WorldHeight);
  const Vec2f InfoLeftScoreOffset(0.02*InfoSize);
  const Vec2f InfoRightScoreOffset(0.95*InfoSize.x(),0.02*InfoSize.y());
  void* InfoFont = GLUT_STROKE_ROMAN;
  const float InfoFontScale = 0.01*0.5*InfoSize.y();
  const Vec3f InfoBorderColor(1,1,1);

};

void drawString(const string& str) {
   for(int i = 0; i < str.length(); i++) {
     glutStrokeCharacter(Params::InfoFont,str[i]);
  }
}


// used to indicate which wall is hit
enum  WallNames {NO_WALL, LOWER_WALL, RIGHT_WALL, LEFT_WALL, UPPER_WALL};

class Court { 
public: 
  Court(const Vec2f& lowerleft, const Vec2f& size): _ll(lowerleft), _ur(lowerleft+size) {}


  const Vec2f& ll() const { return _ll; }
  const Vec2f& ur() const { return _ur; }

private:
 Vec2f _ll, _ur; 

}; 


class Ball { 
public:
  Ball(float r, const Vec2f& c, const Vec2f& v, const Vec3f& col): _radius(r), _center(c), _velocity(v), _color(col) {} 
  void draw() {
    glColor3fv(_color);
    glBegin(GL_POLYGON);    
    for(int i = 0; i < Params::BallNumSegments; i++)
      glVertex2f( _center.x() + _radius*cos(2*M_PI*i/float(Params::BallNumSegments)),  
      _center.y() + _radius*sin(2*M_PI*i/float(Params::BallNumSegments)));
    glEnd();  
  }

  bool intersectSegment(const Vec2f& p1, const Vec2f& p2) { 
    Vec2f v = p2 - p1; 
    Vec2f w = _center - p1; 
    float v_len = v.l2();
    // components of w parallel and perpendicular to the line from p1 to p2
    Vec2f w_parall = v*w.dot(v)/v_len/v_len; 
    Vec2f w_perp = w - w_parall; 
    float d = w_perp.l2();  // distance to the line through p1 and p2
    // approximate test, assume the ball does not go far past the line
    return (d <= _radius) && (w_parall.dot(v) >= 0) && (w_parall.l2() <= v_len);  
  }

  void updateVelocity(const Vec2f& p1, const Vec2f& p2) { 
    Vec2f v = p2 -p1; 
    Vec2f u_perp = _velocity - v*_velocity.dot(v)/v.l2()/v.l2();
    Vec2f n(-v.y(),v.x());
    if( n.dot(_velocity) < 0 ) _velocity = _velocity - 2*u_perp; 
  }

  void updatePosition() {
    _center += _velocity;  
  }

  WallNames collideRectangle(const Vec2f& ll, const Vec2f& ur) { 
    Vec2f lr(ur.x(), ll.y()); 
    Vec2f ul(ll.x(), ur.y());
    if( intersectSegment(ll,lr) )      { updateVelocity(ll,lr); return LOWER_WALL; } 
    else if( intersectSegment(lr,ur) ) { updateVelocity(lr,ur); return RIGHT_WALL;  } 
    else if( intersectSegment(ur,ul) ) { updateVelocity(ur,ul); return UPPER_WALL; }
    else if( intersectSegment(ul,ll) ) { updateVelocity(ul,ll); return LEFT_WALL; }
    else return NO_WALL;
  }

  void collideSegment(const Vec2f& p1, const Vec2f& p2) { 
    if( intersectSegment(p1,p2) ) { updateVelocity(p1,p2); }
  }

  float radius() const { return _radius; }
  const Vec2f& center() const { return _center; }

private:
  float _radius;
  Vec2f _center;
  Vec2f _velocity;
  Vec3f _color; 
};


class Paddle { 
public: 
  enum PaddleDirections {PADDLE_UP, PADDLE_DOWN};

  Paddle(const Vec2f& pos, const Vec2f& size, const Vec2f v, const Vec3f& col): _ll(pos-0.5*size),_ur(pos+0.5*size),
    _velocity(v), _color(col),_offset(0,0), _moving(false), _dir(PADDLE_UP) { }
  

  void draw() {  
    glColor3fv(_color); 
    glPushMatrix();
    glTranslatef(_offset.x(),_offset.y(),0); 
    glRectfv(_ll, _ur);
    glPopMatrix();
  }

  void updatePosition() {
    if(_moving) _offset += (_dir == PADDLE_UP)? _velocity:(-_velocity);
  }

  void setDirection(PaddleDirections d) { 
    _dir = d; 
    _moving = true;
  }

  Vec2f ll() const { return _ll+_offset; }
  Vec2f ur() const { return _ur+_offset; } 

  void stop() { _moving = false; }
  
private:
  Vec2f _offset; 
  Vec2f _ll, _ur; // corners 
  Vec2f _velocity; 
  PaddleDirections _dir; 
  bool _moving; 
  Vec3f _color; 
};


class InfoBoard {
public: 
  InfoBoard(): _leftscore(0),_rightscore(0) {}
  void draw() { 
    glLineWidth(3.0);
    glColor3fv(Params::InfoBorderColor);
    glBegin(GL_LINES);
    glVertex2fv(Params::InfoLowerLeftCorner+ Vec2f(0,Params::InfoSize.y()));
    glVertex2fv(Params::InfoLowerLeftCorner+ Params::InfoSize);
    glEnd();
    glPushMatrix();
      glTranslatef(Params::InfoLowerLeftCorner.x(),Params::InfoLowerLeftCorner.y(),1);
      glPushMatrix();
        glTranslatef(Params::InfoLeftScoreOffset.x(),Params::InfoLeftScoreOffset.y(),0 );
        glScalef(Params::InfoFontScale,Params::InfoFontScale,1);
        ostrstream sleft;
        sleft << _leftscore;
        glColor3fv(Params::PaddleLeftColor);
        drawString(sleft.str());
      glPopMatrix();
      glPushMatrix();
        glTranslatef(Params::InfoRightScoreOffset.x(),Params::InfoRightScoreOffset.y(),0 );
        glScalef(Params::InfoFontScale,Params::InfoFontScale,1);
        ostrstream sright;
        sright << _rightscore;
        glColor3fv(Params::PaddleRightColor);
        drawString(sright.str());
      glPopMatrix();
    glPopMatrix();

  }
  void incleft()  {_leftscore++; } 
  void incright() {_rightscore++; } 

private:
  int   _leftscore,_rightscore; 
};


void Reshape(int width, int height) {

  // glViewport defines part of the window we are going to use 
  WindowParams::WindowWidth = width;
  WindowParams::WindowHeight = height;
  glViewport(0,0,width,height);

  // initialize the viewing transformation (camera position) to identity; 
  // this corresponds to camera looking down negative z axis
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();

  // specify the projection transformation (camera parameters);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();

  // this defines a camera with orthogonal projection that captures the 
  // part of the virtual world inside the box from 
  // -WorldWidth/2 to WorldWidth/2 in the X direction 
  // -WorldHeight/2 to WorldHeight/2 in the Y direction 
  // and from 0 to 1 (default in Z direction 
  gluOrtho2D(-Params::WorldWidth/2, Params::WorldWidth/2,-Params::WorldHeight/2,Params::WorldHeight/2); 
}



class Game { 
public:
  Ball   ball;
  Court  court; 
  Paddle leftpaddle, rightpaddle;
  InfoBoard info;

  Game(): ball(Params::BallRadius,Params::BallInitPos,Params::BallInitVelocity, Params::BallColor),
    court(Params::CourtLowerLeftCorner,Params::CourtSize),
        leftpaddle (Params::CourtLowerLeftCorner+
                   Vec2f(Params::PaddleLeftHPos,Params::CourtSize.y()*0.5),
                   Params::PaddleSize,  
                   Params::PaddleVelocity,
                   Params::PaddleLeftColor),
        rightpaddle(Params::CourtLowerLeftCorner + Vec2f(Params::PaddleRightHPos,Params::CourtSize.y()*0.5),
                   Params::PaddleSize, 
                   Params::PaddleVelocity,
                   Params::PaddleRightColor),
        info() {}
  void draw() { 
    ball.draw(); 
    leftpaddle.draw(); 
    rightpaddle.draw();
  info.draw(); 
  }

  void advance() { 
    WallNames collision = ball.collideRectangle(court.ll(),court.ur());
   if(collision == RIGHT_WALL) info.incleft();  
   else if(collision == LEFT_WALL) info.incright();  

    ball.collideSegment(leftpaddle.ur(),Vec2f(leftpaddle.ur().x(),leftpaddle.ll().y()));
    ball.collideSegment(rightpaddle.ll(), Vec2f(rightpaddle.ll().x(),rightpaddle.ur().y()));
    ball.updatePosition();
    leftpaddle.updatePosition(); 
    rightpaddle.updatePosition(); 
  }

};

Game game;

// gets called when a key is pressed down
void Keyboard( unsigned char key, int, int) { 
  // start motion of paddles
  switch(toupper(key)) { 
  case Params::PaddleLeftKeyUp:    game.leftpaddle.setDirection(Paddle::PADDLE_UP); break;
  case Params::PaddleLeftKeyDown:  game.leftpaddle.setDirection(Paddle::PADDLE_DOWN); break;
  case Params::PaddleRightKeyUp:   game.rightpaddle.setDirection(Paddle::PADDLE_UP); break;
  case Params::PaddleRightKeyDown: game.rightpaddle.setDirection(Paddle::PADDLE_DOWN); break;
  };

}

// gets called when a key is released
void KeyboardUp( unsigned char key, int, int) { 
 switch(toupper(key) ) { 
   // stop motion of paddles on corresponding key releases
  case Params::PaddleLeftKeyUp:    
  case Params::PaddleLeftKeyDown:  game.leftpaddle.stop(); break;
  case Params::PaddleRightKeyUp:   
  case Params::PaddleRightKeyDown: game.rightpaddle.stop(); break;
  };
 
}


void Draw() {
  // set color to initialize the window to; the 4th component 
  // is the alpha value and is not used unless blending is enabled
  glClearColor( 0.5, 0.5,0.5,0.0);
  glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
  game.draw();

  // all drawing is done in the back buffer; 
  // the next function swaps the back and front buffer
  glutSwapBuffers();
}


void Animate(int time) {
  game.advance();
  // timer callback needs to be reinstalled each time 
  glutTimerFunc(Params::TimerStep,Animate,0);
  glutPostRedisplay();
}



int main(int argc, char* argv[]) {

  // initialize glut and parse command-line aguments that glut understands
  glutInit(&argc, argv);

  // initialize dislay mode: 4 color components, double buffer and depth buffer
  glutInitDisplayMode(GLUT_RGBA|GLUT_DOUBLE|GLUT_DEPTH);

  glutInitWindowSize(WindowParams::WindowWidth,WindowParams::WindowHeight);


  WindowParams::MainWindow = glutCreateWindow("Pong");

  // register all callbacks

  // gets called whenever the window needs to be redrawn
  glutDisplayFunc(Draw); 
  // gets called whenever the window changes shape
  glutReshapeFunc(Reshape);
  glutKeyboardFunc(Keyboard);    
  glutKeyboardUpFunc(KeyboardUp);
  // this insures that Animate is called the first time
  glutTimerFunc(Params::TimerStep,Animate,0);
  
  // this is an infinite loop get event - dispatch event which never returns
  glutMainLoop();
  return 0;
}











