/**
 * \defgroup Snake Snake, a casual game including a demo mode.
 *
 * @{
 */

/**
 * @file snake_game.c
 * @brief Implementation of the snake game.
 * @author Peter Fuhrmann, Martin Ongsiek, Daniel Otte, Christian Kroll
 */

#include <assert.h>
#include <stdint.h>
#include <stdbool.h>
#include "../../config.h"
#include "../../compat/pgmspace.h"
#include "../../pixel.h"
#include "../../random/prng.h"
#include "../../util.h"
#include "../../joystick/joystick.h"
#include "../../menu/menu.h"
#include "../../scrolltext/scrolltext.h"
#include "snake_game.h"


#if defined MENU_SUPPORT && defined GAME_SNAKE
// snake icon (MSB is leftmost pixel)
static const uint8_t icon[8] PROGMEM =
	{0xff, 0x81, 0xbd, 0xa5, 0xa5, 0xad, 0xa1, 0xbf};

game_descriptor_t snake_game_descriptor __attribute__((section(".game_descriptors"))) =
{
	&snake_game,
	icon,
};
#endif


/**
 * If defined, joystick controls are NOT as "seen" by the snake but absolute,
 * that is, if pressing up, snake goes up, etc.
 */
#define SNAKE_NEWCONTROL

#if !defined USNAKE_MAX_LENGTH || defined DOXYGEN
	/** The maximum length of the snake. */
	#define USNAKE_MAX_LENGTH 64u
#endif

#if !defined SNAKE_MAX_APPLES || defined DOXYGEN
	/** The maximum number of apples lying on the playing field. */
	#define SNAKE_MAX_APPLES 10
#endif

#if !defined SNAKE_GAME_DELAY || defined DOXYGEN
	/** Delay (in ms) between every state change. */
	#define SNAKE_GAME_DELAY 200
#endif

#if !defined SNAKE_ANIM_DELAY || defined DOXYGEN
	/** Delay (in ms) between every state change. */
	#define SNAKE_ANIM_DELAY 100
#endif

#if !defined SNAKE_TERMINATION_DELAY || defined DOXYGEN
	/** Delay (in ms) between every disappearing pixel of a dying snake. */
	#define SNAKE_TERMINATION_DELAY 60
#endif

/** The color of the surrounding border. */
#define SNAKE_COLOR_BORDER 3
/** The color of the snake. */
#define SNAKE_COLOR_PROTAGONIST 3
/** The color of the apples. */
#define SNAKE_COLOR_APPLE 3


/**
 * Directions of the snake.
 */
enum snake_dir_e
{
	SNAKE_DIR_UP,   /**< Snake is heading up. */
	SNAKE_DIR_RIGHT,/**< Snake is heading right. */
	SNAKE_DIR_DOWN, /**< Snake is heading down. */
	SNAKE_DIR_LEFT, /**< Snake is heading left. */
	SNAKE_DIR_NONE  /**< Helper value for a "resting" joystick. */
};
#ifdef NDEBUG
	typedef uint8_t snake_dir_t;
#else
	typedef enum snake_dir_e snake_dir_t;
#endif

/**
 * This structure represents the snake character itself. It keeps track of the
 * snake's segments, its head and tail and the direction it is heading.
 */
typedef struct snake_protagonist_s
{
	pixel aSegments[USNAKE_MAX_LENGTH]; /**< All segments of the snake. */
	uint8_t nHeadIndex;                 /**< Index of the head segment. */
	uint8_t nTailIndex;                 /**< Index of the tail segment. */
	snake_dir_t dir;                    /**< Direction of the snake.    */
	bool bJoystickLocked;               /**< Avoid double joytick queries
	                                         between movements. */
} snake_protagonist_t;


/**
 * This structure keeps track of all apples which are on the playing field.
 */
typedef struct snake_apples_s
{
	pixel aApples[SNAKE_MAX_APPLES]; /**< Positions of all existing apples. */
	uint8_t nAppleCount;             /**< Count of currently existing apples. */
} snake_apples_t;


/**
 * This function returns the next position which is calculated from a given
 * (current) position and a direction.
 * @param pxNext The position we're going to leave.
 * @param dir The direction that we are heading.
 * @return The next position according the given direction.
 */
static pixel snake_nextDirection(pixel const pxNext,
                                 snake_dir_t const dir)
{
	assert(dir < 4);
	static int8_t const nDelta[] = {0, -1, 0, 1, 0};
	return (pixel){pxNext.x + nDelta[dir], pxNext.y + nDelta[dir + 1]};
}


/**
 * This function draws a border around the playing field.
 */
static void snake_drawBorder(void)
{
#if NUM_COLS == NUM_ROWS
	for (uint8_t i = NUM_COLS; i--;)
	{
		setpixel((pixel){i, 0}, SNAKE_COLOR_BORDER);
		setpixel((pixel){i, NUM_ROWS - 1}, SNAKE_COLOR_BORDER);
		setpixel((pixel){0, i}, SNAKE_COLOR_BORDER);
		setpixel((pixel){NUM_COLS -1, i}, SNAKE_COLOR_BORDER);
	}
#else
	for (uint8_t x = NUM_COLS; x--;)
	{
		setpixel((pixel){x, 0}, SNAKE_COLOR_BORDER);
		setpixel((pixel){x, NUM_ROWS - 1}, SNAKE_COLOR_BORDER);
	}
	for (uint8_t y = NUM_ROWS; y--;)
	{
		setpixel((pixel){0, y}, SNAKE_COLOR_BORDER);
		setpixel((pixel){NUM_COLS - 1, y}, SNAKE_COLOR_BORDER);
	}
#endif
}

#ifdef GAME_SNAKE
/**
 * This function translates hardware port information into joystick directions.
 * @return The current direction of the joystick.
 * @see snake_dir_e
 */
static snake_dir_t snake_queryJoystick(void)
{
	snake_dir_t dirJoystick;
	if (JOYISUP)
	{
		dirJoystick = SNAKE_DIR_UP;
	}
	else if (JOYISRIGHT)
	{
		dirJoystick = SNAKE_DIR_RIGHT;
	}
	else if (JOYISDOWN)
	{
		dirJoystick = SNAKE_DIR_DOWN;
	}
	else if (JOYISLEFT)
	{
		dirJoystick = SNAKE_DIR_LEFT;
	}
	else
	{
		dirJoystick = SNAKE_DIR_NONE;
	}

	return dirJoystick;
}
#endif

/**
 * This function initializes the structure which represents the snake itself.
 * @param pprotSnake The pointer the protagonist structure to be initialized.
 */
static void snake_initGameProtagonist(snake_protagonist_t *pprotSnake)
{
#if NUM_ROWS > NUM_COLS
	pprotSnake->aSegments[0] =
			(pixel){(NUM_COLS - 2) / 2 + 1, (NUM_ROWS - 2) / 2 + 1};
	pprotSnake->aSegments[1] =
			(pixel){(NUM_COLS - 2) / 2 + 1, (NUM_ROWS - 2) / 2};
	pprotSnake->dir = SNAKE_DIR_DOWN;
#else
	pprotSnake->aSegments[0] =
			(pixel){(NUM_COLS - 2) / 2 + 1, (NUM_ROWS - 2) / 2 + 1};
	pprotSnake->aSegments[1] =
			(pixel){(NUM_COLS - 2) / 2 + 2, (NUM_ROWS - 2) / 2 + 1};
	pprotSnake->dir = SNAKE_DIR_RIGHT;
#endif
	pprotSnake->nTailIndex = 0;
	pprotSnake->nHeadIndex = 1;
	pprotSnake->bJoystickLocked = false;
}

#ifdef GAME_SNAKE
/**
 * Determines the next direction of the snake depending on the joystick's input.
 * @param pprotSnake A pointer to the structure of the protagonist.
 * @param pdirLast Last joystick direction to recognize prolonged key presses.
 */
static void snake_userControl(snake_protagonist_t *pprotSnake,
                              snake_dir_t *pdirLast)
{
	snake_dir_t dirJoystick = snake_queryJoystick();
#ifdef SNAKE_NEWCONTROL
	if (dirJoystick != SNAKE_DIR_NONE)
	{
		// valid transitions can only be uneven
		if (((pprotSnake->dir + dirJoystick) & 0x01) &&
				!pprotSnake->bJoystickLocked)
		{
			pprotSnake->dir = dirJoystick;
		}
		// we query the joystick twice as fast as we move the snake, so we
		// have to ensure that it does not bite its head with its head...uh
		pprotSnake->bJoystickLocked = true;
	}
#else
	if ((dirJoystick ^ *pdirLast) && (dirJoystick != SNAKE_DIR_NONE))
	{
		// only left or right movements are valid
		if (dirJoystick & 0x01)
		{
			// rotate through directions (either clockwise or counterclockwise)
			pprotSnake->dir = (pprotSnake->dir +
					(dirJoystick == SNAKE_DIR_LEFT ? 3 : 1)) % 4u;
		}
		// we query the joystick twice as fast as we move the snake, so we
		// have to ensure that it does not bite its head with its head...uh
		pprotSnake->bJoystickLocked = true;
	}
	*pdirLast = dirJoystick;
#endif
}
#endif


#ifdef ANIMATION_SNAKE
/**
 * This function approximates the next direction which may lead to an apple
 * (with a particular probability).
 * @param pprotSnake A pointer to the hungry protagonist.
 * @param pApples A pointer to a bunch of apples.
 */
static void snake_autoRoute(snake_protagonist_t *pprotSnake,
                            snake_apples_t *pApples)
{
	pixel pxHead = pprotSnake->aSegments[pprotSnake->nHeadIndex];
	if (random8() < 80)
	{
		uint8_t nNextApple = 0;
		if (pApples->nAppleCount)
		{
			uint8_t nMinDist = UINT8_MAX;
			for (uint8_t i = 0; i < pApples->nAppleCount; ++i)
			{
				uint8_t nDistX;
				if (pxHead.x > pApples->aApples[i].x)
				{
					nDistX = pxHead.x - pApples->aApples[i].x;
				}
				else
				{
					nDistX = pApples->aApples[i].x - pxHead.x;
				}

				uint8_t nDistY;
				if (pxHead.y > pApples->aApples[i].y)
				{
					nDistY = pxHead.y - pApples->aApples[i].y;
				}
				else
				{
					nDistY = pApples->aApples[i].y - pxHead.y;
				}

				if ((nDistX + nDistY) < nMinDist)
				{
					nMinDist = nDistX + nDistY;
					nNextApple = i;
				}
			}
			if (pprotSnake->dir ^ 0x01) // vertical direction?
			{
				pprotSnake->dir = pApples->aApples[nNextApple].x > pxHead.x ?
						SNAKE_DIR_LEFT : SNAKE_DIR_RIGHT;
			}
			else
			{
				pprotSnake->dir = pApples->aApples[nNextApple].y > pxHead.y ?
						SNAKE_DIR_DOWN : SNAKE_DIR_UP;
			}
		}
	}

	for (uint8_t i = 0; i < 4; ++i)
	{
		pixel pxTest = snake_nextDirection(pxHead, pprotSnake->dir);
		if (get_pixel(pxTest))
		{
			for (uint8_t j = 0; j < pApples->nAppleCount; ++j)
			{
				if ((pxTest.x == pApples->aApples[j].x) &&
						(pxTest.y == pApples->aApples[j].y))
				{
					return;
				}
			}
			pprotSnake->dir = (pprotSnake->dir + 1u) % 4u;
		}
		else
		{
			break;
		}
	}
}
#endif


/**
 * Small animation that lets the dying snake disappear piece by piece.
 * @param pprotSnake A pointer to the dying snake.
 */
static void snake_eliminateProtagonist(snake_protagonist_t *pprotSnake)
{
	while (pprotSnake->nTailIndex != pprotSnake->nHeadIndex)
	{
		clearpixel(pprotSnake->aSegments[pprotSnake->nTailIndex++]);
		pprotSnake->nTailIndex %= USNAKE_MAX_LENGTH;
		wait(SNAKE_TERMINATION_DELAY);
	}
}


/**
 * Initializes the structure that keeps track of all currently existing apples.
 * @param pApples Pointer to the set of apples in question.
 */
static void snake_initApples(snake_apples_t *pApples)
{
	pApples->nAppleCount = 0;
}


/**
 * Checks for an apple at a given position and removes it if there is one.
 * @param pApples The set of apples which are lying on the playing field.
 * @param pxHead The position to be tested.
 * @return false if no apples were found, true otherwise
 */
static bool snake_checkForApple(snake_apples_t *pApples, pixel pxHead)
{
	for (uint8_t i = pApples->nAppleCount; i--;)
	{
		if ((pxHead.x == pApples->aApples[i].x) &&
				(pxHead.y == pApples->aApples[i].y))
		{
			for (; i < pApples->nAppleCount; ++i)
			{
				pApples->aApples[i] = pApples->aApples[i + 1];
			}
			--pApples->nAppleCount;
			return true;
		}
	}
	return false;
}


/**
 * Creates some new apples from time to time.
 * @param pApples Pointer to a set of apples.
 */
static void snake_spawnApples(snake_apples_t *pApples)
{
	if ((pApples->nAppleCount < SNAKE_MAX_APPLES) && (random8() < 10))
	{
		pixel pxApple = (pixel){(random8() % (NUM_COLS-2)) + 1,
								   (random8() % (NUM_ROWS - 2)) + 1};
		if (!get_pixel(pxApple))
		{
			pApples->aApples[pApples->nAppleCount++] = pxApple;
		}
	}
}


/**
 * The main loop (plus initialization) that both drives the game and the
 * demo mode.
 * @param bDemoMode 0 indicates game mode, 1 indicates demo mode
 */
void snake_engine(uint8_t bDemoMode)
{
	// init
	snake_protagonist_t protSnake;
	snake_initGameProtagonist(&protSnake);
	snake_apples_t apples;
	snake_initApples(&apples);
	snake_dir_t dirLast = SNAKE_DIR_NONE;

	// init screen
	clear_screen(0);
	snake_drawBorder();

	for (uint8_t nTick = 0; true; nTick ^= SNAKE_COLOR_APPLE)
	{
		// determine new direction
#if defined ANIMATION_SNAKE && defined GAME_SNAKE
		if (bDemoMode)
		{
			snake_autoRoute(&protSnake, &apples);
		}
		else
		{
			snake_userControl(&protSnake, &dirLast);
		}
		if (bDemoMode || nTick) {
#elif defined ANIMATION_SNAKE
		snake_autoRoute(&protSnake, &apples);
		{
#else
		snake_userControl(&protSnake, &dirLast);
		if (nTick) {
#endif
			// release joystick lock
			protSnake.bJoystickLocked = false;

			// actually move head
			pixel pxOldHead = protSnake.aSegments[protSnake.nHeadIndex];
			protSnake.nHeadIndex = (protSnake.nHeadIndex + 1u) % USNAKE_MAX_LENGTH;
			protSnake.aSegments[protSnake.nHeadIndex] =
					snake_nextDirection(pxOldHead, protSnake.dir);

			// look if we have found an apple
			if (!snake_checkForApple(&apples,
					protSnake.aSegments[protSnake.nHeadIndex]))
			{
				// quit game if we hit something which is not an apple
				if (get_pixel(protSnake.aSegments[protSnake.nHeadIndex]))
				{
					snake_eliminateProtagonist(&protSnake);
					return;
				}

				// remove last segment
				clearpixel(protSnake.aSegments[protSnake.nTailIndex])
				protSnake.nTailIndex =
						(protSnake.nTailIndex + 1u) % USNAKE_MAX_LENGTH;

				// new apples
				snake_spawnApples(&apples);
			}
			// draw new head
			setpixel(protSnake.aSegments[protSnake.nHeadIndex],
					SNAKE_COLOR_PROTAGONIST);
		}

		// draw apples
		for (uint8_t i = apples.nAppleCount; i--;)
		{
			// nTick also serves as blinking color
			setpixel(apples.aApples[i], nTick);
		}

#if defined ANIMATION_SNAKE && defined GAME_SNAKE
		if (bDemoMode)
		{
			wait(SNAKE_ANIM_DELAY);
		}
		else
		{
			wait(SNAKE_GAME_DELAY / 2);
		}
#elif defined ANIMATION_SNAKE
		wait(SNAKE_ANIM_DELAY);
#else
		wait(SNAKE_GAME_DELAY / 2);
#endif
	}
}

/**
 * Snake in game mode.
 */
void snake_game(void)
{
	snake_engine(0);
#if defined GAME_SNAKE && defined SCROLLTEXT_SUPPORT
	scrolltext("</#Game Over!");
#endif
}

/*@}*/
