#include <nds.h>
#include <bitset>
#include <stdio.h>
#include <vector>
#include <deque>
#include <array>
#include <consts.h>
#include <maxmod9.h>
#include <nds/ndstypes.h>

#include "soundbank.h"
#include "soundbank_bin.h"

#include "tilemap.h"

using namespace std;

volatile uint8_t frame = 0;
volatile uint16_t ticks = 0;
volatile bool internals = false;

std::deque<std::array<int, 3>> snake;

std::vector<std::array<int, 2>> apples;
std::vector<std::array<int, 2>> bad_apples;

volatile uint16_t snakelength;
/***
 * 0 = up
 * 1 = right
 * 2 = down
 * 3 = left
 ***/
volatile uint16_t snakeDirection;

volatile uint16_t score;

bool gameOver = false;
bool paused = false;

bool musicEnabled = true;
bool hardMode = false;

// 31x23
PrintConsole topScreen;
PrintConsole bottomScreen;

int sound;

mm_sound_effect sfx_pickup;
mm_sound_effect sfx_boom;
mm_sound_effect sfx_pause;
mm_sound_effect sfx_unpause;
mm_sound_effect sfx_plop;
mm_sound_effect sfx_fail;

//Sprites
u16* gfx_segment; 
u16* gfx_segment_small; 
u16* gfx_head; 
u16* gfx_apple;
u16* gfx_tail;
u16* gfx_apple_rotten;

const void * get_sprite_pointer(uint16 spriteIndex) {
	return &tilemapTiles[spriteIndex*16];
}

void setup() {
	// Set up screens
	
	videoSetMode(MODE_0_2D);
	videoSetModeSub(MODE_0_2D);
	
	vramSetBankC(VRAM_C_SUB_BG);
	vramSetBankD(VRAM_D_SUB_SPRITE);
	consoleInit(&topScreen, 3,BgType_Text4bpp, BgSize_T_256x256, X_MAX, 0, true, true);
	consoleInit(&bottomScreen, 3,BgType_Text4bpp, BgSize_T_256x256, X_MAX, 0, false, true);
	oamInit(&oamSub, SpriteMapping_1D_32, false);

	gfx_segment = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color);
	gfx_segment_small = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color);
	gfx_head = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color);
	gfx_apple = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color);
	gfx_apple_rotten = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color);
	gfx_tail = oamAllocateGfx(&oamSub, SpriteSize_8x8, SpriteColorFormat_256Color);

	dmaCopy(get_sprite_pointer(1),gfx_segment,64);
	dmaCopy(get_sprite_pointer(2),gfx_segment_small,64);
	dmaCopy(get_sprite_pointer(3),gfx_head,64);
	dmaCopy(get_sprite_pointer(5),gfx_apple,64);
	dmaCopy(get_sprite_pointer(11),gfx_apple_rotten,64);

	consoleSelect(&topScreen);
	

	dmaCopy(tilemapPal,SPRITE_PALETTE_SUB,tilemapPalLen);

	mmInitDefaultMem((mm_addr)soundbank_bin);
	// Background Music
	mmLoad( MOD_0RIGIN );

	// Load SFX
	mmLoadEffect( SFX_332629__TREASURESOUNDS__ITEM_PICKUP );
	mmLoadEffect( SFX_BOOM );
	mmLoadEffect( SFX_167127__CRISSTANZA__PAUSE );
	mmLoadEffect( SFX_167126__CRISSTANZA__UNPAUSE );
	mmLoadEffect( SFX_273792__PACOMAV__PLOP );
	mmLoadEffect( SFX_73750__TIMBRE__REMIX_OF_BENBONCAN_SAD_TROMBONE_MORE_WAH_BRIGHT_DE_CLICKED );


	sfx_pickup = {
		{ SFX_332629__TREASURESOUNDS__ITEM_PICKUP } ,			// id
		(int)(1.0f * (1<<10)),	// rate
		0,		// handle
		255,	// volume
		128,	// panning
	};

	sfx_boom = {
		{ SFX_BOOM } ,
		(int)(1.0f * (1<<10)),
		1,
		255,
		128,
	};

	sfx_pause = {
		{ SFX_167127__CRISSTANZA__PAUSE } ,
		(int)(1.0f * (1<<10)),
		2,
		255,
		128,
	};

	sfx_unpause = {
		{ SFX_167126__CRISSTANZA__UNPAUSE } ,
		(int)(1.0f * (1<<10)),
		3,
		255,
		128,
	};

	sfx_plop = {
		{ SFX_273792__PACOMAV__PLOP } ,
		(int)(1.0f * (1<<10)),
		4,
		128,
		128,
	};

	sfx_fail = {
		{ SFX_73750__TIMBRE__REMIX_OF_BENBONCAN_SAD_TROMBONE_MORE_WAH_BRIGHT_DE_CLICKED } ,
		(int)(1.0f * (1<<10)),
		4,
		255,
		128,
	};

}


void check_collision_self() {

	std::array<int, 3> headpos = snake.front();
	
	std::deque<std::array<int, 3>>::iterator testit = snake.begin();
	testit++;	//skip head, no collision here
	for(testit; testit != snake.end(); testit++)
	{
		std::array<int, 3> dot = *testit;
		if ((dot[0] == headpos[0] && dot[1] == headpos[1] ) && !gameOver) {
			gameOver = true;
			mmStop();
		}
	}
	
}

void check_collision_apples() {

	std::array<int, 3> headpos = snake.front();
	
	std::vector<std::array<int, 2>>::iterator testit = apples.begin();
	for(testit; testit <= apples.end(); testit++)
	{
		std::array<int, 2> apple = *testit;
		if ((apple[0] == headpos[0] && apple[1] == headpos[1] )) {
			score += 10;
			snakelength++;
			apples.erase(testit);
			mmEffectEx(&sfx_pickup);
		}
	}
	
}

void check_collision_apples_rotten() {

	std::array<int, 3> headpos = snake.front();
	
	std::vector<std::array<int, 2>>::iterator testit = bad_apples.begin();
	for(testit; testit <= bad_apples.end(); testit++)
	{
		std::array<int, 2> apple = *testit;
		if ((apple[0] == headpos[0] && apple[1] == headpos[1] )) {
			snakelength--;
			bad_apples.erase(testit);
			mmEffectEx(&sfx_boom);
			if (snakelength < 2) {
				gameOver = true;
			}
		}
	}
	
}

void draw_snake_head(uint16 &oamId) {
	std::array<int, 3> dot = snake.front();
	bool hflip = false;
	bool vflip = false;
	switch (snakeDirection)
	{
		case 0: //going up
			dmaCopy(&tilemapTiles[64],gfx_head,64);
			break;

		case 1:	//going right
			dmaCopy(&tilemapTiles[48],gfx_head,64);
			break;

		case 2: //going down
			dmaCopy(&tilemapTiles[64],gfx_head,64);
			vflip = true;
			break;

		case 3: //going left
			/*for(int i = 0; i < 8 * 8; i=i+2)
			{
				gfx_head[i/2] = ((BITMAP_HEAD_RIGHT[i+1]<<8) | BITMAP_HEAD_RIGHT[i]);
			}*/
			dmaCopy(&tilemapTiles[48],gfx_head,64);
			hflip = true;
			break;
		
		default:
			break;
	}
	oamSet(&oamSub,	// oam
			oamId,		//id
			dot[0]*8,		// x
			dot[1]*8,		// y
			0, 		// priority
			0,		// palette_alpha
			SpriteSize_8x8, // size
			SpriteColorFormat_256Color,  // color format
			gfx_head,	// gfxoffset
			-1,		// affineIndex
			false,	// sizeDouble
			false,	// hide
			hflip,	// hflip
			vflip,	// vflip
			false	// mosaic
			);
			oamId++;
}

void draw_snake_tail(uint16 &oamId) {
	std::array<int, 3> dot = snake.back();
	bool hflip = false;
	bool vflip = false;
	switch (snake.at(snake.size()-2).at(2))
	{
		case 0: //going up
			dmaCopy(get_sprite_pointer(9+((snake.size()-1)%2)),gfx_tail,64);
			vflip = true;
			break;

		case 1:	//going right
			dmaCopy(get_sprite_pointer(7+((snake.size()-1)%2)),gfx_tail,64);
			hflip = true;
			break;

		case 2: //going down
			dmaCopy(get_sprite_pointer(9+((snake.size()-1)%2)),gfx_tail,64);
			break;

		case 3: //going left
			/*for(int i = 0; i < 8 * 8; i=i+2)
			{
				gfx_tail[i/2] = ((BITMAP_tail_RIGHT[i+1]<<8) | BITMAP_tail_RIGHT[i]);
			}*/
			dmaCopy(get_sprite_pointer(7+((snake.size()-1)%2)),gfx_tail,64);
			break;
		
		default:
			break;
	}
	oamSet(&oamSub,	// oam
			oamId,		//id
			dot[0]*8,		// x
			dot[1]*8,		// y
			0, 		// priority
			0,		// palette_alpha
			SpriteSize_8x8, // size
			SpriteColorFormat_256Color,  // color format
			gfx_tail,	// gfxoffset
			-1,		// affineIndex
			false,	// sizeDouble
			false,	// hide
			hflip,	// hflip
			vflip,	// vflip
			false	// mosaic
			);
	oamId++;
}

void draw_snake(uint16 &oamId) {
	draw_snake_head(oamId);
	for(size_t i = 1; i < snake.size()-1; i++)
	{		
		if(i%2) {
			oamSet(&oamSub,	// oam
				oamId,		//id
				snake[i][0]*8,		// x
				snake[i][1]*8,		// y
				0, 		// priority
				0,		// palette_alpha
				SpriteSize_8x8, // size
				SpriteColorFormat_256Color,  // color format
				gfx_segment_small,	// gfxoffset
				-1,		// affineIndex
				false,	// sizeDouble
				false,	// hide
				false,	// hflip
				false,	// vflip
				false	// mosaic
				); 
		} else {
			oamSet(&oamSub,	// oam
				oamId,		//id
				snake[i][0]*8,		// x
				snake[i][1]*8,		// y
				0, 		// priority
				0,		// palette_alpha
				SpriteSize_8x8, // size
				SpriteColorFormat_256Color,  // color format
				gfx_segment,	// gfxoffset
				-1,		// affineIndex
				false,	// sizeDouble
				false,	// hide
				false,	// hflip
				false,	// vflip
				false	// mosaic
				); 
		}
		oamId++;
	}
	draw_snake_tail(oamId);
}

void draw_apples(uint16 &oamId) {
	
	for(std::array<int, 2> apple : apples)
	{
		oamSet(&oamSub,	// oam
			oamId,		//id
			apple[0]*8,		// x
			apple[1]*8,		// y
			0, 		// priority
			0,		// palette_alpha
			SpriteSize_8x8, // size
			SpriteColorFormat_256Color,  // color format
			gfx_apple,	// gfxoffset
			-1,		// affineIndex
			false,	// sizeDouble
			false,	// hide
			false,	// hflip
			false,	// vflip
			false	// mosaic
			);
		oamId++;
	}
	
}

void draw_apples_rotten(uint16 &oamId) {
	
	for(std::array<int, 2> apple : bad_apples)
	{
		oamSet(&oamSub,	// oam
			oamId,		//id
			apple[0]*8,		// x
			apple[1]*8,		// y
			0, 		// priority
			0,		// palette_alpha
			SpriteSize_8x8, // size
			SpriteColorFormat_256Color,  // color format
			gfx_apple_rotten,	// gfxoffset
			-1,		// affineIndex
			false,	// sizeDouble
			false,	// hide
			false,	// hflip
			false,	// vflip
			false	// mosaic
			);
		oamId++;
	}
	
}

void draw_game() {
	uint16 oamid = 0;
	if (!gameOver) {
		draw_apples(oamid);
		draw_apples_rotten(oamid);
		draw_snake(oamid);
	} 
	else
	{
		iprintf("\x1b[11;12HGAME OVER!");
		mmStop();
		mmEffectEx(&sfx_fail);
		consoleSelect(&topScreen);
		iprintf("\x1b[10;13HGAME OVER!");
	}
	/*iprintf("\x1b[7;0HoamIDmax=%i",oamid);*/
	
}

void draw_stats() {
	if ( internals ) {
		iprintf("\x1b[0;0HFrame = %i\nTick = %i\ndir = %i", frame, ticks, snakeDirection);
		iprintf("\x1b[3;0HLength: %i", snakelength);
		iprintf("\x1b[4;0Hx=%i\ny=%i",snake.front()[0], snake.front()[1]);
	}
	iprintf("\x1b[10;12HScore: %i", score);
	
	if (paused) {
		
		if (musicEnabled) {
			iprintf("\x1b[23;0HX = Disable Music");
		} else {
			iprintf("\x1b[23;0HX = Enable Music ");
		}

		if (hardMode) {
			iprintf("\x1b[22;0HY = Restart in normal mode");
		} else {
			iprintf("\x1b[22;0HY = Restart in Hard mode");
		}
		
	}
	
}

void handle_vblank() {
	frame++;
	consoleSelect(&topScreen);
	draw_stats();
	oamUpdate(&oamSub);
	//flashy_colors();
}


void update_snake() {
	
	std::array<int, 3> headpos = snake.front();
	std::array<int, 3> newHead;
	if (snakeDirection == 0) {
		newHead = { headpos[0], headpos[1]-1, snakeDirection };
	} 
	else if (snakeDirection == 1)
	{
		newHead = { headpos[0]+1, headpos[1], snakeDirection };
	} 
	else if (snakeDirection == 2)
	{
		newHead = { headpos[0], headpos[1]+1, snakeDirection };
	} 
	else if (snakeDirection == 3)
	{
		newHead = { headpos[0]-1, headpos[1], snakeDirection };
	}
	
	if (newHead[0] > X_MAX) {
		newHead[0] = 0;
	}

	if (newHead[1] > Y_MAX) {
		newHead[1] = 0;
	}

	if (newHead[0] < 0) {
		newHead[0] = X_MAX;
	}

	if (newHead[1] < 0) {
		newHead[1] = Y_MAX;
	}
	snake.push_front(newHead);
	snake.resize(snakelength);
	
}

void spawn_apples() {
	
	if (apples.size() < 1 ) {
		std::array<int,2> apple;
		apple[0] = rand() % (X_MAX+1);
		apple[1] = rand() % (Y_MAX+1);
		apples.push_back(apple);
	}
	
}

void spawn_apples_rotten() {
	if ( (ticks%10==0) && hardMode && ((rand() % 10)<10) && (bad_apples.size()<20)) {
		std::array<int,2> badApple;
		badApple[0] = rand() % (X_MAX+1);
		badApple[1] = rand() % (Y_MAX+1);
		bad_apples.push_back(badApple);
	}
}

void blink_apples() {
	
	if (ticks % 4) {
		dmaCopy(get_sprite_pointer(5),gfx_apple,64);
	} else {
		dmaCopy(get_sprite_pointer(6),gfx_apple,64);
	}
}

void do_tick() {
	if (!paused) {
		ticks++;
		if (!gameOver){
			
			/*if (hardMode) {
				timerStop(0);
				timerStart(0, ClockDivider_1024, TIMER_FREQ_1024(ticks+5), do_tick);
			}*/
			
			update_snake();
			mmEffectEx(&sfx_plop);
			check_collision_self();
			check_collision_apples();
			check_collision_apples_rotten();
			spawn_apples();
			spawn_apples_rotten();
			blink_apples();
			//consoleSelect(&bottomScreen);
			draw_game();
		} 
	}
}
	
void init_game() {
	
	if (hardMode) {
		timerStart(0, ClockDivider_1024, TIMER_FREQ_1024(9), do_tick);
		setBackdropColorSub(RGB15(10,5,5));
		score = 0;
		snakelength = START_LENGTH_HARD;
		snake = { {2,1,1}, {1,1,1} };
		apples.clear();
		bad_apples.clear();
		snakeDirection = 1;
		ticks = 0;
		gameOver=false;
		consoleSelect(&topScreen);
		consoleClear();
		consoleSelect(&bottomScreen);
		consoleClear();
		oamClear(&oamSub,0,0);
		mmStart( MOD_0RIGIN, MM_PLAY_LOOP );
		if (!musicEnabled) {
			mmPause();
		}
	} 
	else
	{
		timerStart(0, ClockDivider_1024, TIMER_FREQ_1024(5), do_tick);
		setBackdropColorSub(RGB15(5,5,5));
		score = 0;
		snakelength = START_LENGTH;
		snake = { {4,1,1}, {3,1,1}, {2,1,1}, {1,1,1} };
		snakeDirection = 1;
		ticks = 0;
		gameOver=false;
		consoleSelect(&topScreen);
		consoleClear();
		consoleSelect(&bottomScreen);
		consoleClear();
		oamClear(&oamSub,0,0);
		mmStart( MOD_0RIGIN, MM_PLAY_LOOP );
		if (!musicEnabled) {
			mmPause();
		}
	}
	
	
	 
}

int main(void) {

	// Set up VBlank handler
	irqSet(IRQ_VBLANK, handle_vblank);

	setup();
	init_game();

	// Start timer for game ticks

 
	while(1) {

		scanKeys();

		int keys = keysDown();

		if (keys && internals) {
			consoleSelect(&topScreen);
			iprintf("\x1b[6;0Hkeys = %s", std::bitset<16>(keys).to_string().c_str());
		}
		
		if ((keys & KEY_START) && (keys & KEY_L) ) break;

		if (keys & KEY_SELECT) {
			internals = !internals;
			if (internals) {
				setBackdropColor(RGB15(0,0,7));
			} else {
				setBackdropColor(RGB15(0,0,0));
				consoleSelect(&topScreen);
				consoleClear();
			}
		}

		if (!paused && !gameOver) {
			if (keys & KEY_UP) {
				snakeDirection = 0;			
				draw_game();
			}

			if (keys & KEY_RIGHT) {
				snakeDirection = 1;
				draw_game();
			}

			if (keys & KEY_DOWN) {
				snakeDirection = 2;
				draw_game();
			}

			if (keys & KEY_LEFT) {
				snakeDirection = 3;
				draw_game();
			}
		}

		if (paused) {
			if (keys & KEY_X) {
				musicEnabled = !musicEnabled;
			}
			if (keys &  KEY_Y) {
				hardMode = !hardMode;
				init_game();
				draw_game();
			}
		}

		if (keys & KEY_R) {
			lcdSwap();
		}


		if (keys & (KEY_START)) {
			if (!gameOver) {
				paused = !paused;
				if (paused) {
					consoleSelect(&topScreen);
					mmEffectEx(&sfx_pause);
					if ( musicEnabled ) {
						mmPause();
					}
					iprintf("\x1b[11;12HPaused");
				} 
				else
				{
					if (musicEnabled) {
						mmResume();
					}
					mmEffectEx(&sfx_unpause);
					consoleSelect(&topScreen);
					consoleClear();
				}
				
			} 
			else
			{
				init_game();
			}
			
		}
		
	}

	return 0;
}
