/* 
   bugs / known issues
  - Makefile for project doesn't check dependencies for cs20200 properly
  - gdb doesn't have line-numbers, etc. for this file (depending on order of make'ing)

  Note - some random comments/thoughts/advice are througout.  Search for "note"
 */

#ifndef OBJECTS_HPP
#define OBJECTS_HPP

using namespace std;

#include<list>
#include<vector>
#include<fstream>
#include<iostream>
#include<sstream>
#include<assert.h>
#include<unistd.h>
#include<string.h>
#include<time.h>

/* *** Class definitions *** */

class World;
class Game;


// Object - has a location, velocity, whether can move or not
class Object {
public:
  double x, vx; // x pos and velocity, kept as double rather than int so "real" position can move fractionally
  double y, vy; // y pos and velocity
  bool moves;   // is this object able to move
  char ch;      // character for displaying
  bool isMoving_y; // is moving in y direction or not
  int num;      // used for different things depending on object type
  bool flagToRemove; // if should remove, set to true

  Object(char ch); // constructor
  void update_pos(double g, double delta_s, Game &G); // update position/velocity
  virtual string state(); // string summary of state
};


// Player - is an object, also has coins and lives
class Player : public Object {
private:
  int lives;
  
public:
  int coins;

  Player();
  string state(); // overrides the default state function, to include coins, lives
  void addCoin();
  int loseLife();
  int getLives();
};


// World - keeps track of all objects, movement
class World {
public:
  double g,            // gravity, #rows/sec down
    delta_s;           // interval between updates to velocity/position
  int width, height;   // of the whole world
  list<Object *> objs; // linked list of objects
  Object *** grid;     // 2d array of objects
  Game &G;             // for having access to rest of the game
  
  World(Game &G);      // constructor
  void update();       // update positions, etc.
  void add_player();   // add players before adding any other objects
};


// Screen - for drawing/display
class Screen {
public:
  int max_x, max_y; // dimensions of screen
  bool colors_ok;   // were we able to initialize colors and such
  Game &G;     

  Screen(Game &G);
  ~Screen();        // destructor, for free'ing
  void draw();      // draw the screen
  char occupied(double y, double x); // is that space occupied
  char barrier(double y, double x);  // is that space a barrier (wall/floor)
  bool same_location(Object *ob1, Object *ob2);
};

// Game - keeps the entire game
class Game {
public:
  World w;   // keeps objects
  Screen s;  // for drawing
  bool gameOver;
  int removedCount; // for debugging, how many objects have been removed from board
  time_t time_start;
  
  Game();  
  bool load_world(const char *filename);
  void play_game();
  void handle_key(int ch);
  void moveObject(Object *obj, double new_y, double new_x); // move object, updating things as needed
  bool isPlayer(Object *obj);
};







/* *** Class method defitions *** */



/* Object method definitions */

// ch(ch) is like this->ch = ch
Object::Object(char ch) : ch(ch) {
  vx = 0; vy = 0; moves = false;
  isMoving_y = false; num = 0;
  flagToRemove = false;
}

// update position and velocities based on gravity, velocities
// return false if should be removed
void Object::update_pos(double g, double delta_s, Game &G)  {
  if (moves) {
    // y and vy
    
    double x_new, y_new;

    // update y velocity based on gravity, and y position
    if (isMoving_y) {
      vy = vy + (g * delta_s);
      y_new = y + vy * delta_s; // where we would end up
    }
    else
      y_new = y;

    // update x position
    x_new = x + vx * (delta_s);

    // check if our new position would have run into something
    if (G.s.barrier(y_new, x_new)) { // can't move there
      vy = 0;
      
      // if y was going down, reset vy to 0 and not moving y
      if ((int) y_new < (int) y) {
	isMoving_y = false;
      }

      // if y was going up and barrier is a & then take coins
      if ((int) y_new > (int) y &&
	  G.s.barrier(y_new, x_new) == '&') {
	Object *obj = G.w.grid[(int) y_new][(int) x_new];
	if (obj != NULL && obj->num > 0) {
	  obj->num = obj->num - 1;
	  if (obj->num == 0) obj->ch = '=';
	  Player *p = (Player *) *(G.w.objs.begin());
	  p->coins ++;
	}
      }

      // if x was changing, make vx bounce
      if ((int) x_new != (int) x)
	vx *= -1;
      
    }
    else { 
      G.moveObject(this, y_new, x_new);
    }
  }
  else {
    // for non-moving object, just put it on the grid to make sure
    // the grid is updated in case some moving object overwrote this one
    G.w.grid[(int)y][(int)x] = this;
  }
}

string Object::state() {
  ostringstream oss; 
  // x, y, etc. for debugging
  oss << "(x, y) = (" << x << ", " << y << ") " <<
    "(vx, vy) = (" << vx << ", " << vy << ") " <<
    "moves=" << moves;
  return oss.str();
}


/* Player method definitions */

Player::Player() : Object('M') {
  coins = 0;
  lives = 3;
  x = 0;
  y = 0;
  vy = 0;
  moves = true;
}

string Player::state() {
  string s = Object::state();
  ostringstream oss;
  oss << " " << ch << ": coins=" << coins << " lives=" << lives;
  // skip showing x,y, etc. except for debugging
  //return s + oss.str();
  return oss.str();
}

void Player::addCoin() {
  coins++;
}

// note - good OO design - use methods to get/change state of the object,
//  and make the actual lives variable as private so other code doesn't
//  need to worry about the internal details.
int Player::loseLife() {
  lives = lives > 0 ? lives-1 : 0;
  return lives;
}

int Player::getLives() {
  return lives;
}


/* World method definitions */

World::World(Game &G) : G(G){
  // gravity - tradeoff between this and the vy that is set on a keyup
  g = -60;
  delta_s = 0.01;
  width = height = 0;
  grid = NULL;
}

void World::update() {
  // the player
  Player *pl = (Player *) *(objs.begin());
  
  // update positions of objects - call the update function on each
  for(list<Object *>::iterator it= objs.begin(); it != objs.end(); it++) {
    (*it)->update_pos(g, delta_s, G);
  }

  // go through and remove the ones that were flagged for removal
  for(list<Object *>::iterator it= objs.begin(); it != objs.end(); ) {
    Object *obj = *it;
    if (obj->flagToRemove) {
      grid[(int) obj->y][(int) obj->x] = NULL;
      delete obj;
      it = objs.erase(it);
    }
    else
      it++;
  }
  
  // note - we are storing all of the obects in both the objs list and the
  // grid.  we could just as well have iterated through the grid to call
  // update on all of the objects.
}

void World::add_player() {
  Player *p = new Player();
  objs.push_back(p);
}


/* Screen method definitions */

// color definitions, see https://jonasjacek.github.io/colors/
//   assuming your terminal has the same color definitions, these
//   choices are reasonable looking colors.
#define MARIO_BLUE  19
#define MARIO_RED   160
#define MARIO_GREEN 40
#define MARIO_GOLD  221
#define MARIO_WHITE 15
#define MARIO_BLACK 0

// color in curses uses color pairs, and there are two different places where a magic
//  code needs to be used.  define our magic codes here so we are consistent when we use
//  them.  See below...
typedef enum {MARIO_GREEN_BLUE=1, MARIO_RED_BLUE, MARIO_GOLD_BLUE, MARIO_BLACK_BLUE, MARIO_WHITE_BLUE} MARIO_COLOR_PAIRS;

// Constructor for screen - get all of the curses things setup
Screen::Screen(Game &G) : G(G) {
  // note - already done from main.c:
  //  initsrc (creates screen)
  //  noecho (don't display chars that are typed)
  //  cbreak (don't wait for enter key)
  //  curs_set(0) - makes cursor not visible

  // set colors_ok = false if any of the color things give an error condition
  colors_ok = true;
  if (start_color() == ERR) colors_ok = false;

  // it seems init_color doesn't work on some terminals (mine on mac anyway).
  // it seems the default colors are - https://jonasjacek.github.io/colors/
  //  unless you changed them, so I picked colors from the defaults above that look okay on
  //  my terminal (still need to try on others...
  attron(A_BOLD); // everything bold
  if (init_pair(MARIO_GREEN_BLUE, MARIO_GREEN, MARIO_BLUE) == ERR) colors_ok = false;
  if (init_pair(MARIO_RED_BLUE, MARIO_RED, MARIO_BLUE) == ERR) colors_ok = false;
  if (init_pair(MARIO_GOLD_BLUE, MARIO_GOLD, MARIO_BLUE) == ERR) colors_ok = false;
  if (init_pair(MARIO_BLACK_BLUE, MARIO_BLACK, MARIO_BLUE) == ERR) colors_ok = false;
  if (init_pair(MARIO_WHITE_BLUE, MARIO_WHITE, MARIO_BLUE) == ERR) colors_ok = false;
  
  // screen setup
  nodelay(stdscr, TRUE); // make getch return even if no character typed
  keypad(stdscr, TRUE);  // enable use of arrow keys.

  // max x and y based on the size of the screen
  max_x = COLS-3;
  max_y = LINES-3;
}

// descructor - cleanup
Screen::~Screen() {
  endwin();
  refresh();
}

// main function for drawing the screen.  note that it also
// checks for collisions and such.  this is probably bad OO design - should
// separate out the collision detection and drawing.
void Screen::draw() {
  // erase the screen
  bkgd(COLOR_PAIR(MARIO_RED_BLUE));
  erase();

  // border and such
  attron(COLOR_PAIR(MARIO_RED_BLUE));
  border('|', '|', '-', '-', '+', '+', '+', '+');
  attroff(COLOR_PAIR(MARIO_RED_BLUE));

  // information at the top
  attron(COLOR_PAIR(MARIO_WHITE_BLUE));
  mvprintw(1, 1,"q to quit, left/right arrows move, up arrow to jump");
  
  // player object should be first on the objs list - get info, print about it
  assert(G.w.objs.size() > 0);
  Player *p = (Player *)(*(G.w.objs.begin()));
  int y, x; char ch;
  y = (int) p->y; x = (int) p->x; ch = p->ch;
  mvprintw(2, 1, "%s", p->state().c_str());

  // some more information about the world
  mvprintw(3, 1, "world height, width ~ %d, %d, %d sec", G.w.height, G.w.width, time(NULL) - G.time_start);
  mvprintw(4, 1, "removedCount %d", G.removedCount);
  //mvprintw(4, 1, "%d, %d, %d, %d. colors_ok %d.  %d", can_change_color(), has_colors(), COLORS, COLOR_PAIRS, colors_ok == true, COLOR_WHITE);
  attroff(COLOR_PAIR(MARIO_WHITE_BLUE));

  // non-player world objects, start at 1, go through to display each one
  for(list<Object *>::iterator it= ++G.w.objs.begin(); it != G.w.objs.end(); ++it) {
    int obj_y = (int) (*it)->y, obj_x = (int) (*it)->x;
    char obj_ch = (*it)->ch;
    
    // what color should object be, default to red
    short color = MARIO_RED_BLUE;
    switch(obj_ch) {
    case '$':
    case '&':
      color = MARIO_GOLD_BLUE;
      break;
    case '@':
      color = MARIO_RED_BLUE;
      break;
    case '_':
      color = MARIO_BLACK_BLUE;
      break;
    case '=':
    case '|':
	color = MARIO_GREEN_BLUE;
	break;      
    }

    // draw it
    attron(COLOR_PAIR(color));
    mvprintw(max_y - (int) (*it)->y + 1, // y=0 is at bottom, switch around, leave 1 for border
	     (int) (*it)->x + 1, // don't draw on border
	     "%c", (*it)->ch);
    attroff(COLOR_PAIR(color));
  }
  
  // now draw player
  attron(COLOR_PAIR(MARIO_WHITE_BLUE));
  mvprintw(max_y - y + 1, x + 1, "%c", ch); // see comments above
  attroff(COLOR_PAIR(MARIO_WHITE_BLUE));

  // if game over, let them know
  if (G.gameOver) {
    mvprintw(max_y/2, max_x/3, "~~~ GAME OVER ~~~");
  }

  // nothing gets redrawn on the screen until calling refresh
  refresh();
}

// return non-zero if x, y is hitting something (e.g. '=')
char Screen::barrier(double y, double x) {
  // off screen, return 1
  if ((int) y < 0 || (int) y >= G.w.height ||
      (int) x < 0 || (int) x >= G.w.width) return 1;

  // is the spot occupied
  char ch = occupied(y, x);

  switch(ch) {
  case '=':
  case '|':
  case '&':
  case '_':
    return ch;
  }
  
  return 0;
}

char Screen::occupied(double y, double x) {
  int xx = (int)x + 1; // translate from game coordinates to screen coordinates
  int yy = max_y - (int)y + 1;

  // read that position on the screen
  char ch = mvinch((int) yy, (int) xx);

  if (ch == ' ') return 0;
  return ch;
}

bool Screen::same_location(Object *ob1, Object *ob2) {
  if (ob1 == NULL || ob2 == NULL) return false;

  if ((int) ob1->x == (int) ob2->x &&
      (int) ob1->y == (int) ob2->y)
    return true;
  else
    return false;
}


/* Game method definitions */

// note: gameOver(false) is like gameOver = false;
//       w{World(*this)} is like I declared World w(*this);
Game::Game() : w{World(*this)}, s{Screen(*this)}, gameOver(false), removedCount(0) {
  w.add_player(); // add players before loading any other objects
  time_start = time(NULL);
}

// return true if loaded, false if not
bool Game::load_world(const char *filename) {
  ifstream in(filename); // somewhere in there it does something like fopen or open

  vector<string> lines;
  
  // check that file was opened
  if (! in.good()) {
    cout << "Unable to open " << filename << " for reading." << endl;
    return false;
  }
  
  // loop to read from input, into vector (aka array) of lines
  string line; 
  unsigned max_len = 0;
  while (getline(in, line)) {
    if (line.length() > max_len) max_len = line.length();
    lines.push_back(line);
  }
  in.close();

  // height and width of the world
  w.height = lines.size();
  w.width = max_len;

  // allocate 2d array for keeping track of things, height x width, pointers to Object
  w.grid = new Object**[w.height];
  for(int y=0; y < w.height; y++) {
    w.grid[y] = new Object*[w.width];
    for(int x=0; x < w.width; x++)
      w.grid[y][x] = NULL;
  }

  // scan through the lines, creating objects and putting into world
  // will create Object's and put both into objs linked list and 2d grid
  for(unsigned i=0; i < lines.size(); i++) {
    for(unsigned j=0; j < lines[i].length(); j++) {
      char ch = lines[i][j];
      
      // for this character read from the world file, create an object for anything
      // that is not a space or newline
      if (ch != ' ' && ch != '\n') {
	// all objects have a char, x, and y
	Object * ob = new Object(ch);
	ob->x = j;
	ob->y = lines.size()-1 - i; // last line is height 0, first line is lines.size()-1

	// some objects have other parameters to set
	switch(ch) {
	case '@':
	  ob->vx = 3;
	  if (rand() % 2) ob->vx *= -1; // will be left or right
	  ob->moves = true;
	  break;
	case '&':
	  ob->num = 4; // 4 coins
	  break;
	}

	// put object in our objs list and our 2d grid
	w.objs.push_back(ob);
	w.grid[(int) ob->y][(int) ob->x] = ob; // save pointer in the grid as well
      }
    }    
  }

  // success
  return true;
}


// main game loop - draw, get key, handle key, update world. repeat
void Game::play_game() {
  int chTyped;
  do {
    s.draw();

    chTyped = getch();
    usleep(w.delta_s * 1000000);
    if (! gameOver) {
      handle_key(chTyped);
      w.update();
    }
    
  } while (chTyped != 'q');
}

// take key user typed and do whatever we should do
void Game::handle_key(int ch) {
  // player obj is always first in the linked list
  Player *p = (Player *) (*(w.objs.begin()));

  // to keep track if we moved the player
  int new_x = -1;
  
  switch(ch) {
  case KEY_UP:
    // start jump by setting y velocity, if not already in middle of jump
    if (! p->isMoving_y) {
      p->vy = 25; // #rows up / sec
      p->isMoving_y = true;
    }
    break;
    
  case KEY_DOWN: // don't do anything, for now
    break;
    
  case KEY_LEFT:
    // move one to left if not on edge of screen
    new_x = p->x > 0 ? p->x-1 : 0;
    moveObject(p, p->y, new_x);
    break;
    
  case KEY_RIGHT:
    // move one to right if not on edge of screen
    new_x = p->x < s.max_x ? p->x+1 : s.max_x;
    moveObject(p, p->y, new_x);
    break;
  }
}

void Game::moveObject(Object *obj, double new_y, double new_x) {
  if (obj == NULL) return;
  
  // don't move if would be a barrier
  if (s.barrier(new_y, new_x)) return;
  
  // if x changed, check for falling y
  if ((int) obj->x != (int) new_x) {
    if (!s.barrier(new_y-1, new_x))
      obj->isMoving_y = true;
  }

  // check for collision between obj and something there already
  Object *other_obj = w.grid[(int) new_y][(int) new_x];
  Player *p = (Player *) *(w.objs.begin());
  if (other_obj != NULL && 
      other_obj != obj &&
      ! obj->flagToRemove && // only take one coin or life, in case we flagged already
      ! other_obj->flagToRemove &&
      (p == obj || other_obj == p)) {
    // something there that is not the player, deal with it
    char c = p == obj ? other_obj->ch : obj->ch;
    switch (c) {
    case '@':
      // if it was player moving and jumped on it, then don't lose life
      if (p == obj && (int)new_y == (int)p->y -1)
	;
      else {
	p->loseLife();
	if (p->getLives() <= 0) gameOver = true;
      }
      break;
    case '$':
      p->coins += 1;
      break;
    }
    // and flag it for removal
    if (p != obj)
      obj->flagToRemove = true;
    else if (p != other_obj)
      other_obj->flagToRemove = true;
    removedCount++;
  }
  
  // now, actually move it
  w.grid[(int) obj->y][(int) obj->x] = NULL; // not where it was anymore
  obj->y = new_y;
  obj->x = new_x;
  w.grid[(int) obj->y][(int) obj->x] = obj;
}

bool Game::isPlayer(Object *obj) {
  if (obj == *(w.objs.begin())) return true;
  else return false;
}

#endif