/*
CS 349 A1 Skeleton Code - Snake

- - - - - - - - - - - - - - - - - - - - - -

Commands to compile and run:

    g++ -o snake snake.cpp -L/usr/X11R6/lib -lX11 -lstdc++
    ./snake

Note: the -L option and -lstdc++ may not be needed on some machines.
*/

#include <iostream>
#include <list>
#include <vector>
#include <cstdlib>
#include <sys/time.h>
#include <math.h>
#include <stdio.h>
#include <unistd.h>

/*
 * Header files for X functions
 */
#include <X11/Xlib.h>
#include <X11/Xutil.h>

using namespace std;

// Globals for xlib
Colormap colours;
XColor xcolour;
char black[] = "rgb:00/00/00";
char white[] = "rgb:ff/ff/ff";
char lblue[] = "rgb:00/ff/ff";
char orange[] = "rgb:FF/45/00";

/*
 * Global game state variables
 */
const int Border = 1;
const int BufferSize = 10;
int FPS = 30;
const int width = 800;
const int height = 600;
const int blockSize = 25;
int velocity = 5;
bool paused = true;
bool starting = true;
bool dead = false;

/*
 * Information to draw on the window.
 */
typedef struct XInfo
{
	Display	* display;
	int screen;
	Window window;
	GC gc[3];
	int width;		// size of window
	int height;
  Pixmap buffer;
} Xinfo;


// get microseconds
unsigned long now()
{
	timeval tv;
	gettimeofday(&tv, NULL);
	return tv.tv_sec * 1000000 + tv.tv_usec;
}

/*
 * Function to put out a message on error exits.
 */
void error(string str)
{
  cerr << str << endl;
  exit(0);
}

// change the drawing coloursvoid changecolour(char colour[])
void changecolour(XInfo & xinfo, char colour[])
{
  XParseColor(xinfo.display, colours, colour, &xcolour);
  XAllocColor(xinfo.display, colours, &xcolour);
  XSetForeground(xinfo.display, xinfo.gc[0], xcolour.pixel);
  XFreeColormap(xinfo.display, colours);
}

/*
 * An abstract class representing displayable things.
 */
class Displayable
{
	public:
		virtual void paint(XInfo & xinfo) = 0;
};

class Fruit : public Displayable
{
	public:
		virtual void paint(XInfo & xinfo)
    {
      changecolour(xinfo, orange);
			XFillArc(xinfo.display, xinfo.buffer, xinfo.gc[0], x, y, 25, 25, 0, 360 * 64);
    }

    Fruit()
    {
      newspot();
    }

    int getx()
    {
      return x;
    }

    int gety()
    {
      return y;
    }

    void newspot()
    {
      x = (rand() % 32) * 25;
      y = (rand() % 24) * 25;
    }
        // ** ADD YOUR LOGIC **
        /*
         * The fruit needs to be re-generated at new location every time a snake eats it. See the assignment webpage for more details.
         */

    private:
        int x;
        int y;
};

class Section: public Displayable
{
	public:
		virtual void paint(XInfo & xinfo)
    {
      changecolour(xinfo, black);
      switch(direction)
      {
        case 1:
          XFillRectangle(xinfo.display, xinfo.buffer, xinfo.gc[0], x, y, blockSize, (len + 1) * blockSize);
          break;

        case 2:
          XFillRectangle(xinfo.display, xinfo.buffer, xinfo.gc[0], (x - len * blockSize), y, (1 + len) * blockSize, blockSize);
          break;

        case 3:
          XFillRectangle(xinfo.display, xinfo.buffer, xinfo.gc[0], x, (y - len * blockSize), blockSize, ((1 + len) * blockSize));
          break;

        case 4:
          XFillRectangle(xinfo.display, xinfo.buffer, xinfo.gc[0], x, y, (len + 1) * blockSize, blockSize);
          break;
      }
		}

    void increment(int x)
    {
      len += x;
    }

    void decrement()
    {
      --len;
    }

		bool empty()
    {
      return (!len);
    }

    void grow()
    {
      switch(direction)
      {
        case 1:
          y -= blockSize;
          break;

        case 2:
          x += blockSize;
          break;

        case 3:
          y += blockSize;
          break;

        case 4:
          x -= blockSize;
          break;
      }

      ++len;
    }

    int getx()
    {
      return x;
    }

    int gety()
    {
      return y;
    }

    int getdir()
    {
      return direction;
    }

    int getlen()
    {
      return len;
    }

		Section(int x, int y, int dir, int len = 1): x(x), y(y), direction(dir), len(len)
    {
      blockSize = 25;
		}

	private:
		int x;
    int y;
    int len;
		int blockSize;
		int direction;
};

class Snake : public Displayable
{
	public:
		virtual void paint(XInfo & xinfo)
    {
      for (Section * each: sections)
      {
        each->paint(xinfo);
      }

		}

		void move(XInfo & xinfo)
    {
      if ((now() - lastmove < speed) || paused)
      {
        return;
      }
      // turn if a turn happens
      if (direction)
      {
        Section * newsec = new Section(sections.front()->getx(), sections.front()->gety(), direction, 0);
        direction = 0;
        sections.insert(sections.begin(), newsec);
      }

      // move 1 block forwards
      sections.front()->grow();

      // see if fruit was eaten
      if (sections.front()->getx() == fruity->getx() && sections.front()->gety() == fruity->gety())
      {
        fruit = 1;
      }

      // add fruit, remove 1 from end, delete if empty
      sections.back()->increment(fruit);
      if (fruit) fruity->newspot();
      fruit = 0;
      sections.back()->decrement();
      if (sections.back()->empty())
      {
        delete sections.back();
        sections.pop_back();
      }

      if (sections.front()->getx() > width || sections.front()->gety() < 0)
      {
        dead = true;
        paused = true;
      }

      if (sections.front()->getx() < 0 || sections.front()->gety() > height)
      {
        dead = true;
        paused = true;
      }

      if (sections.size() <= 3) goto SKIP;
      for (auto each = sections.begin() + 1; each != sections.end(); ++each)
      {
        int tempdir = (*each)->getdir();
        int frontx = sections.front()->getx();
        int fronty = sections.front()->gety();
        int eachx = (*each)->getx();
        int eachy = (*each)->gety();
        int eachlen = (*each)->getlen();

        switch(tempdir)
        {
          case 1:
            if (frontx == eachx && (fronty >= eachy && fronty <= (eachy + blockSize * eachlen)))
            {
              dead = true;
              paused = true;
            }
            break;

          case 2:
            if ((frontx >= (eachx - eachlen * blockSize) && frontx <= eachx) && fronty == eachy)
            {
              dead = true;
              paused = true;
            }
            break;

          case 3:
            if (frontx == eachx && (fronty <= eachy && fronty >= (eachy - blockSize * eachlen)))
            {
              dead = true;
              paused = true;
            }
            break;

          case 4:
            if ((frontx <= (eachx - eachlen * blockSize) && frontx >= eachx) && fronty == eachy)
            {
              dead = true;
              paused = true;
            }
            break;
        }
      }

      SKIP:
      lastmove = now();
		}

    void moveup()
    {
      if (direction == 4 || direction == 2 || direction == 0) direction = 1;
    }

    void moveright()
    {
      if (direction == 1 || direction == 3 || direction == 0) direction = 2;
    }

    void movedown()
    {
      if (direction == 4 || direction == 2 || direction == 0) direction = 3;
    }

    void moveleft()
    {
      if (direction == 1 || direction == 3 || direction == 0) direction = 4;
    }

    void changespeed(unsigned long x)
    {
      speed = 250000 / x;
    }

    void reset(int fast)
    {
      int size = sections.size();

      for (int i = 0; i < size; i++)
      {
        delete sections.at(i);
        sections.erase(sections.begin() + i);
      }

      Section * newsec = new Section(x, y, 2, 2);
      sections.insert(sections.begin(), newsec);
      direction = 0;
      blockSize = 25;
      speed = 250000 / fast;
      lastmove = 0;
    }

		Snake(int x, int y, Fruit * fruity, unsigned long fast): x(x), y(y), fruity(fruity)
    {
      Section * newsec = new Section(x, y, 2, 2);
      sections.insert(sections.begin(), newsec);
      direction = 0;
      blockSize = 25;
      speed = 250000 / fast;
      lastmove = 0;
		}

    ~Snake()
    {
      int size = sections.size();

      for (int i = 0; i < size; i++)
      {
        delete sections.at(i);
        sections.erase(sections.begin() + i);
      }
    }

	private:
		int x;
		int y;
    vector<Section *> sections;
		int blockSize;
		int direction;
    unsigned long speed;
    int fruit;
    Fruit * fruity;
    unsigned long lastmove;
};

list<Displayable *> dList;           // list of Displayables
Fruit fruity;
Snake snake(100, 450, &fruity, velocity);

/*
 * Initialize X and create a window
 */
void initX(int argc, char * argv[], XInfo & xInfo)
{
	XSizeHints hints;
	unsigned long white, black;

   /*
	* Display opening uses the DISPLAY	environment variable.
	* It can go wrong if DISPLAY isn't set, or you don't have permission.
	*/
	xInfo.display = XOpenDisplay("");
	if (!xInfo.display)
  {
		error("Can't open display.");
	}

   /*
	* Find out some things about the display you're using.
	*/
	xInfo.screen = DefaultScreen(xInfo.display);

  colours = DefaultColormap(xInfo.display, xInfo.screen);
	white = XWhitePixel(xInfo.display, xInfo.screen);
	black = XBlackPixel(xInfo.display, xInfo.screen);

	hints.x = 100;
	hints.y = 100;
	hints.width = 800;
	hints.height = 600;
	hints.flags = PPosition | PSize;

	xInfo.window = XCreateSimpleWindow
  (
		xInfo.display,				// display where window appears
		DefaultRootWindow( xInfo.display ), // window's parent in window tree
		hints.x, hints.y,			// upper left corner location
		hints.width, hints.height,	// size of the window
		Border,						// width of window's border
		black,						// window border colour
		white
  );					// window background colour

	XSetStandardProperties
  (
		xInfo.display,		// display containing the window
		xInfo.window,		// window whose properties are set
		"animation",		// window's title
		"Animate",			// icon's title
		None,				// pixmap for the icon
		argv, argc,			// applications command line args
		&hints
  );			// size hints for the window

  int depth = DefaultDepth(xInfo.display, DefaultScreen(xInfo.display));
  xInfo.buffer = XCreatePixmap(xInfo.display, xInfo.window, hints.width, hints.height, depth);
	/*
	 * Create Graphics Contexts
	 */
	int i = 0;
	xInfo.gc[i] = XCreateGC(xInfo.display, xInfo.window, 0, 0);
  XSetBackground(xInfo.display, xInfo.gc[0], WhitePixel(xInfo.display, xInfo.screen));
	XSetFillStyle(xInfo.display, xInfo.gc[i], FillSolid);
	XSetLineAttributes(xInfo.display, xInfo.gc[i],
	                     1, LineSolid, CapButt, JoinRound);

	XSelectInput(xInfo.display, xInfo.window,
		ButtonPressMask | KeyPressMask |
		PointerMotionMask |
		EnterWindowMask | LeaveWindowMask);  // for resize events

	/*
	 * Put the window on the screen.
	 */
	XMapRaised( xInfo.display, xInfo.window );
	XFlush(xInfo.display);
}

void splash(XInfo & xinfo)
{
  changecolour(xinfo, white);
  XFillRectangle(xinfo.display, xinfo.buffer, xinfo.gc[0], 100, 100, 600, 400);
  changecolour(xinfo, black);
  XFontStruct * font = XLoadQueryFont(xinfo.display, "12x24");
  XSetFont(xinfo.display, xinfo.gc[0], font->fid);
  XDrawString(xinfo.display, xinfo.buffer, xinfo.gc[0], 350, 200, "Snake", 5);
  XDrawString(xinfo.display, xinfo.buffer, xinfo.gc[0], 340, 240, "tsdedhar", 8);
  XDrawString(xinfo.display, xinfo.buffer, xinfo.gc[0], 340, 280, "20621325", 8);
  XDrawString(xinfo.display, xinfo.buffer, xinfo.gc[0], 265, 320, "Use arrow keys to turn", 22);
  XDrawString(xinfo.display, xinfo.buffer, xinfo.gc[0], 200, 360, "Collect fruit to grow your snake", 32);
}

void endgame(XInfo & xinfo)
{
  changecolour(xinfo, white);
  XFillRectangle(xinfo.display, xinfo.buffer, xinfo.gc[0], 100, 100, 600, 400);
  changecolour(xinfo, black);
  XFontStruct * font = XLoadQueryFont(xinfo.display, "12x24");
  XSetFont(xinfo.display, xinfo.gc[0], font->fid);
  XDrawString(xinfo.display, xinfo.buffer, xinfo.gc[0], 347, 200, "Game Over!", 10);
  XDrawString(xinfo.display, xinfo.buffer, xinfo.gc[0], 340, 240, "Press r to restart", 18);
  XDrawString(xinfo.display, xinfo.buffer, xinfo.gc[0], 342, 280, "Press q to quit", 15);
}

/*
 * Function to repaint a display list
 */
void repaint(XInfo & xinfo)
{
	list<Displayable *>::const_iterator begin = dList.begin();
	list<Displayable *>::const_iterator end = dList.end();

	// get height and width of window (might have changed since last repaint)
  XWindowAttributes windowInfo;
	XGetWindowAttributes(xinfo.display, xinfo.window, &windowInfo);
  unsigned int height = windowInfo.height;
	unsigned int width = windowInfo.width;
  changecolour(xinfo, lblue);
  XFillRectangle(xinfo.display, xinfo.buffer, xinfo.gc[0], 0, 0, width, height);
  changecolour(xinfo, black);

	// draw display list
	while (begin != end)
  {
		Displayable * d = *begin;
		d->paint(xinfo);
		begin++;
	}

  if (starting) splash(xinfo);
  if (dead)
  {
    endgame(xinfo);
  }
}

void handleKeyPress(XInfo & xinfo, XEvent & event)
{
	KeySym key;
	char text[BufferSize];
	int i = XLookupString
  (
		(XKeyEvent *)&event, 	// the keyboard event
		text, 					// buffer when text will be written
		BufferSize, 			// size of the text buffer
		&key, 					// workstation-independent key symbol
		NULL
  );

	if (i == 1)
  {
		printf("Got key press -- %c\n", text[0]);

    switch(text[0])
    {
      case 'q':
        error("Terminating normally.");
        break;

      case ' ':
        paused = !paused;
        starting = false;
        break;

      case 'r':
        snake.reset(velocity);
        paused = true;
        starting = true;
        dead = false;
        break;
    }
	}

  if (key == XK_Up)
  {
    snake.moveup();
  }

  if (key == XK_Right)
  {
    snake.moveright();
  }

  if (key == XK_Down)
  {
    snake.movedown();
  }

  if (key == XK_Left)
  {
    snake.moveleft();
  }
}

void handleAnimation(XInfo & xinfo, int inside)
{
	snake.move(xinfo);
}

void eventLoop(XInfo & xinfo)
{
	// Add stuff to paint to the display list
	dList.push_front(&snake);
  dList.push_front(&fruity);
	XEvent event;
  unsigned long lastRepaint = 0;
  int inside = 0;
  XWindowAttributes w;
  XGetWindowAttributes(xinfo.display, xinfo.window, &w);

  while(1)
  {
    if (XPending(xinfo.display) > 0)
    {
      XNextEvent(xinfo.display, &event);

      switch(event.type)
      {
        case KeyPress:
          handleKeyPress(xinfo, event);
          break;
        case EnterNotify:
          inside = 1;
          break;
        case LeaveNotify:
          inside = 0;
          break;
      }
    }

    unsigned long end = now();

    if (end - lastRepaint > 1000000 / FPS)
    {
      handleAnimation(xinfo, inside);
      repaint(xinfo);
      XCopyArea(xinfo.display, xinfo.buffer, xinfo.window, xinfo.gc[0], 0, 0 , w.width, w.height, 0, 0);
      XFlush(xinfo.display);
      lastRepaint = now();
    }

    if (XPending(xinfo.display) == 0)
    {
      usleep(1000000 / FPS - (end - lastRepaint));
    }
  }
}


/*
 * Start executing here.
 *	 First initialize window.
 *	 Next loop responding to events.
 *	 Exit forcing window manager to clean up - cheesy, but easy.
 */
int main (int argc, char * argv[])
{
  if (argc == 1)
  {
  }
  else if (argc == 3)
  {
    FPS = atoi(argv[1]);
    velocity = atoi(argv[2]);
    if (atoi(argv[2]) < 1 || atoi(argv[2]) > 10) goto BADARGS;
  }
  else
  {
    BADARGS:
    printf("Incorrect arguments supplied\n");
    exit(0);
  }

  snake.changespeed(velocity);
	XInfo xInfo;
	initX(argc, argv, xInfo);
	eventLoop(xInfo);
	XCloseDisplay(xInfo.display);
}
