//------------------------------------------------------------------------------
//  Simple Demo Snake Game
//
//  Description:
//
//    A simple Win32 snake game.
//    This file has been tested with MingW/Code::Blocks 17.12
//
//    Remember to link with Winmm.lib
//    For MingW/CodeBlocks that means adding -lwinmm flag in linker options
//
//  Author: Andrew Lim
//
//  Last Updated: 4 March, 2019
//------------------------------------------------------------------------------

#include <windows.h>
#include <queue>
#include <algorithm>
#include <ctime>
#include <deque>
using namespace std;

#define MAP_WIDTH   45             //
#define MAP_HEIGHT  30             //  These are pretty self-explanatory.
#define TILE_SIZE   8              //

#define MAX_SCORE   30             //  Units of food to consume to win game.
#define GROWTH_RATE 5              //  Tiles to grow per food.
#define TIMER_DELAY 60             //  Refresh rate in milliseconds.
#define WM_MM_TIMER WM_USER + 1    //  Custom message sent by the timer.


char map[MAP_WIDTH][MAP_HEIGHT];   //  The map.
enum {EMPTY, FOOD, SNAKE};         //  Values the map can have.

HWND          hwndMain;            //  Handle to the game window.
WPARAM        currDirection;       //  0, VK_LEFT, VK_RIGHT, VK_UP or VK_DOWN
queue<WPARAM> directions;          //  Stores arrow key presses.
deque<POINT>  snake;               //  Represent the snake.
int           iTimerId;            //  Multimedia timer id.
int           score;               //  Guess?
int           growCounter;         //  How many tiles snake must grow.

/*
 *  Callback function for the multimedia timer.
 */
void CALLBACK TimeProc(UINT uID, UINT uMsg, DWORD dwUser, DWORD d1, DWORD d2) {
  PostMessage( hwndMain, WM_MM_TIMER, 0, 0 );
}

/*
 *  Starts the multimedia timer.
 */
void StartTimer() {
  TIMECAPS tc;
  timeGetDevCaps(&tc, sizeof(TIMECAPS));
  DWORD resolution = min( max(tc.wPeriodMin, (UINT)0 ),
                          tc.wPeriodMax);
  timeBeginPeriod( resolution );
  iTimerId = timeSetEvent( TIMER_DELAY,resolution,TimeProc,0,TIME_PERIODIC );
}

/*
 *  Stops the multimedia timer.
 */
void StopTimer() {
  timeKillEvent( iTimerId );
  timeEndPeriod( TIMER_DELAY );
}

/*
 *  Returns the opposite direction.
 */
WPARAM Opposite( WPARAM d ) {
  switch(d) {
  case VK_LEFT: return VK_RIGHT;
  case VK_RIGHT: return VK_LEFT;
  case VK_UP: return VK_DOWN;
  case VK_DOWN: return VK_UP;
  default: return 0;
  }
}


/*
 *  Draws a rectangular tile.
 */
void DrawTile( HDC hDC, int x, int y, COLORREF colour ) {
  RECT rc = { x, y, x+TILE_SIZE, y+TILE_SIZE };
  COLORREF oldcr = SetBkColor(hDC, colour);
  ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &rc, "", 0, 0);
  SetBkColor(hDC, oldcr);
}

/*
 *  Adds food at a random position in the map.
 */
void AddFood() {
  int x = rand() % MAP_WIDTH;
  int y = rand() % MAP_HEIGHT;
  while( map[x][y] != EMPTY ) {
    x = rand() % MAP_WIDTH;
    y = rand() % MAP_HEIGHT;
  }
  map[x][y] = FOOD;
}

/*
 *  Draws the game based on map values.
 *  If the score is MAX_SCORE, draw "YOU WIN!" also.
 */
void DrawGame(HDC hDC) {
  for( int row=0; row<MAP_HEIGHT; ++row ) {
    for( int col=0; col<MAP_WIDTH; ++col ) {
      int x = col * TILE_SIZE, y = row * TILE_SIZE;
      if ( map[col][row] == EMPTY )      DrawTile(hDC, x, y, RGB(0,0,0));
      else if ( map[col][row] == SNAKE ) DrawTile(hDC, x, y, RGB(150,150,255));
      else if ( map[col][row] == FOOD )  DrawTile(hDC, x, y, RGB(255,0,0));
    }
  }
  if ( score == MAX_SCORE )  {
    RECT rc;
    GetClientRect(hwndMain,&rc);
    SetTextColor(hDC,RGB(255,255,0));
    SetBkMode(hDC,TRANSPARENT);
    DrawText(hDC,"YOU WIN!",-1,&rc,DT_SINGLELINE|DT_CENTER|DT_VCENTER);
    StopTimer();
  }
}

/*
 *  Resets the game.
 */
void ResetGame() {
  for( int row=0; row<MAP_HEIGHT; ++row ) {
    for( int col=0; col<MAP_WIDTH; ++col ) {
      map[col][row] = EMPTY;
    }
  }

  snake.clear();
  POINT first = { MAP_WIDTH/2, MAP_HEIGHT/2 };
  snake.push_front(first);
  map[first.x][first.y] = SNAKE;

  growCounter = score = currDirection = 0;
  SetWindowText( hwndMain, "Score: 0" );

  AddFood();
}

/*
 *  Updates the snake based on its current direction.
 */
void UpdateGame() {
  if ( !directions.empty() )  {
    if ( directions.front() != Opposite(currDirection) ) {
      currDirection = directions.front();
    }
    directions.pop();
  }

  POINT head = snake.front();

  switch(currDirection){
  case VK_LEFT:  head.x--; break;
  case VK_RIGHT: head.x++; break;
  case VK_UP:    head.y--; break;
  case VK_DOWN:  head.y++; break;
  default: return;
  }

  if ( head.x<0 || head.y<0 || head.x>=MAP_WIDTH || head.y>=MAP_HEIGHT ) {
    ResetGame();
  }
  else if ( map[head.x][head.y] == EMPTY ) {
    if ( growCounter ) {
      --growCounter;
      map[ head.x ][ head.y ] = SNAKE;
      snake.push_front(head);
    }
    else {
      map[ head.x ][ head.y ] = SNAKE;
      snake.push_front(head);
      map[ snake.back().x ][ snake.back().y ] = EMPTY;
      snake.pop_back();
    }
  }
  else if ( map[head.x][head.y] == FOOD ) {
    growCounter += GROWTH_RATE - 1;
    map[ head.x ][ head.y ] = SNAKE;
    snake.push_front(head);
    score++;
    static char szScore[50];
    wsprintf( szScore, "Score: %d", score );
    SetWindowText( hwndMain, szScore );
    if ( score != MAX_SCORE )
      AddFood();
  }
  else if ( map[head.x][head.y] == SNAKE ) {
    ResetGame();
  }
}

/*
 *  Main window's window procedure.
 */
LRESULT CALLBACK WndProc( HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam ) {
  if ( msg == WM_DESTROY ) {
    PostQuitMessage(0);
    return 0;
  }

  else if ( msg == WM_MM_TIMER ) {
    UpdateGame();
    InvalidateRect(hwnd,NULL,FALSE);
  }

  else if ( msg == WM_KEYDOWN ) {
    if (wParam==VK_UP || wParam==VK_DOWN ||wParam==VK_LEFT || wParam==VK_RIGHT) {
      if ( directions.empty() )
        directions.push( wParam );
      else if ( directions.back() != wParam )
        directions.push( wParam );
    }
    return 0;
  }

  else if ( msg == WM_PAINT )  {
    PAINTSTRUCT ps;
    HDC hDC = BeginPaint( hwnd, &ps );
    DrawGame(hDC);
    EndPaint( hwnd, &ps );
    return 0;
  }

  return DefWindowProc(hwnd,msg,wParam,lParam);
}

/*
 *  Program entry-point.
 */
int WINAPI WinMain( HINSTANCE hInst, HINSTANCE hPrev, LPSTR args, int nShow ) {
  srand( (unsigned int)time(0) );
  WNDCLASS wc = {0};
  memset( &wc, 0, sizeof(WNDCLASS) );
  wc.lpszClassName = "LimChongLiangAndrew";
  wc.hInstance     = hInst ;
  wc.lpfnWndProc   = WndProc ;
  wc.hCursor       = LoadCursor(0,IDC_ARROW);
  DWORD dwStyle = WS_OVERLAPPEDWINDOW&~(WS_MAXIMIZEBOX|WS_THICKFRAME);
  RECT rc = {0,0,MAP_WIDTH*TILE_SIZE,MAP_HEIGHT*TILE_SIZE};
  AdjustWindowRect(&rc,dwStyle,FALSE);
  RegisterClass(&wc);
  hwndMain = CreateWindow(wc.lpszClassName,"",dwStyle|WS_VISIBLE,
                          0,0,rc.right-rc.left,rc.bottom-rc.top,0,0,hInst,0);
  ResetGame();
  StartTimer();
  MSG  msg ;
  while( GetMessage(&msg,0,0,0) > 0 ) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
  StopTimer();
  return (int)msg.wParam;
}
