#include "Console.h"

#include <SDL_keycode.h>

#include "Entity.h"
#include "Game.h"
#include "Item.h"
#include "Level.h"
#include "LevelGen.h"
#include "Player.h"
#include "System.h"
#include "Version.h"
#include "Video.h"
#include "World.h"
#include "VM.h"

#define CONSOLEMSG_DISPLAY_TIME 400
#define CONSOLE_MAXSCROLLBACK 30
#define CONSOLE_MAXLINELEN 128

#define CMD_SUCCESS 0
#define CMD_ERROR 1

static char lines[CONSOLE_MAXSCROLLBACK][CONSOLE_MAXLINELEN];
static time consoleMsgTimer = 0;

static char editline[CONSOLE_MAXLINELEN];
static int editlen = 0;

static char lastcmd[CONSOLE_MAXLINELEN];
static int onlast;

int Cmd_Warp(const char *cmdstr);
int Cmd_ToggleSpawn(const char *cmdstr);
int Cmd_Levelup(const char *cmdstr);
int Cmd_RevealMap(const char *cmdstr);
int Cmd_Give(const char *cmdstr);
int Cmd_Summon(const char *cmdstr);
int Cmd_Register(const char *cmdstr);
int Cmd_Cheat(const char *name, int bitmask);
int Cmd_Connect(const char *cmdstr);

void Console_PrintMsg(const char *fmtstr, ...)
{
	// Shift all lines back
	for (int i = CONSOLE_MAXSCROLLBACK - 2; i >= 0; i--)
	{
		SDL_strlcpy(lines[i + 1], lines[i], CONSOLE_MAXLINELEN);
	}

	va_list args;
	va_start(args, fmtstr);
	SDL_vsnprintf(lines[0], CONSOLE_MAXLINELEN, fmtstr, args);
	va_end(args);

	if (!Game.consoleOpen)
		consoleMsgTimer = CONSOLEMSG_DISPLAY_TIME;
}

static int cmdi = 0;
static char *GetNextArg(const char *cmdstr)
{
	static char argbuf[CONSOLE_MAXLINELEN];
	int argi = 0;

	if (cmdstr[cmdi] == '\0')
		return NULL;

	SDL_memset(argbuf, 0, CONSOLE_MAXLINELEN);
	while (!SDL_isspace(cmdstr[cmdi]) && cmdstr[cmdi] != '\0')
	{
		argbuf[argi] = cmdstr[cmdi];
		cmdi++;
		argi++;
	}
	argbuf[argi] = '\0';

	while (SDL_isspace(cmdstr[cmdi]))
		cmdi++;

	return argbuf;
}

int Console_RunCommand(const char *cmdstr)
{
	char *command;

	if (cmdstr == NULL)
		return CMD_SUCCESS;

	Console_PrintMsg("] %s", cmdstr);

	SDL_strlcpy(lastcmd, cmdstr, CONSOLE_MAXLINELEN);
	onlast = 0;

	cmdi = 0;
	command = GetNextArg(cmdstr);
	if (command == NULL)
		return CMD_SUCCESS;

	if (SDL_strcmp(command, "warp") == 0)
		return Cmd_Warp(cmdstr);
	else if (SDL_strcmp(command, "togglespawn") == 0)
		return Cmd_ToggleSpawn(cmdstr);
	else if (SDL_strcmp(command, "levelup") == 0)
		return Cmd_Levelup(cmdstr);
	else if (SDL_strcmp(command, "revealmap") == 0)
		return Cmd_RevealMap(cmdstr);
	else if (SDL_strcmp(command, "give") == 0)
		return Cmd_Give(cmdstr);
	else if (SDL_strcmp(command, "summon") == 0)
		return Cmd_Summon(cmdstr);
	else if (SDL_strcmp(command, "reg") == 0)
		return Cmd_Register(cmdstr);
	else if (SDL_strcmp(command, "noclip") == 0)
		return Cmd_Cheat(command, GAME_CHEAT_NOCLIP);
	else if (SDL_strcmp(command, "god") == 0)
		return Cmd_Cheat("godmode", GAME_CHEAT_GODMODE);
	else if (SDL_strcmp(command, "connect") == 0)
		return Cmd_Connect(cmdstr);
	else
	{
		Console_PrintMsg("Unknown command: '%s'", command);
		return CMD_ERROR;
	}
}

char *Console_GetNewestLine(void)
{
	return lines[0];
}

void Console_HandleKey(int key)
{
	if (key == '`' || key == SDLK_ESCAPE)
	{
		Game.consoleOpen = 0;
	}
	else if (key == SDLK_BACKSPACE && editlen > 0)
	{
		editline[--editlen] = '\0';
	}
	else if (key == SDLK_UP && onlast == 0)
	{
		editlen = SDL_strlcpy(editline, lastcmd, CONSOLE_MAXLINELEN);
		onlast = 1;
	}
	else if (key == SDLK_DOWN && onlast == 1)
	{
		editline[0] = '\0';
		editlen = 0;
		onlast = 0;
	}
	else if (key == SDLK_RETURN)
	{
		Console_RunCommand(editline);
		editline[0] = '\0';
		editlen = 0;
	}
}

void Console_HandleTyping(char *text)
{
	if (*text == '`')
		return;
	if (editlen < CONSOLE_MAXLINELEN - 1)
	{
		editline[editlen++] = text[0];
		editline[editlen] = '\0';
	}
}

static const char *title = "Rovgend's Labyrinth v" RVG_VERSION_STRING " Developer Console";
void Console_Draw(Bmp_t *screen)
{
	int line = 0;
	int editw, edith;

	Bmp_SetActiveFont(BMPFONT_TINY);

	if (!Game.consoleOpen)
	{
		if (consoleMsgTimer > 0)
		{
			Bmp_DrawText(screen, lines[0], 0xff7f7f7f, 2, 2, screen->w - 2);
			consoleMsgTimer--;
		}
		return;
	}

	Bmp_FillRegion(screen, 0x7f102050, 0, 0, screen->w, screen->h / 2);

	Bmp_TextSize(title, &editw, &edith, screen->w);
	Bmp_DrawText(screen, title, 0xff102050, (screen->w - editw) / 2, 2, screen->w);

	while (line < CONSOLE_MAXSCROLLBACK && lines[line][0] != '\0')
	{
		int yy = screen->h / 2 - 16 - line * 8;
		if (yy < 0)
			break;
		Bmp_DrawText(screen, lines[line], 0xffd0d0d0, 2, yy, screen->w);
		line++;
	}

	Bmp_TextSize(editline, &editw, &edith, screen->w);

	Bmp_DrawGlyph(screen, '>', 0xffffffff, 2, screen->h / 2 - 8);
	Bmp_DrawText(screen, editline, 0xffffffff, 8, screen->h / 2 - 8, screen->w);
	Bmp_DrawGlyph(screen, '_', 0xffffffff, 8 + editw, screen->h / 2 - 8);
}

/***************** Command Implementations *****************/

int Cmd_Warp(const char *cmdstr)
{
	char *lvlStr = GetNextArg(cmdstr);
	if (lvlStr == NULL)
	{
		Console_PrintMsg("Must specify a floor", lvlStr);
		return CMD_ERROR;
	}
	int level = SDL_atoi(lvlStr);
	if (level < 1 || level > gNumLevels)
	{
		Console_PrintMsg("Invalid floor number '%s'", lvlStr);
		return CMD_ERROR;
	}
	if (Game.state == GAME_STATE_PLAY || Game.state == GAME_STATE_PAUSED)
	{
		Game_ChangeFloor(level, -1, -1);
	}
	else
	{
		Game_Start(GAME_START_NEW, level);
	}
	Game_SetMenu(NULL);
	Vid_SetMouseLocked(true);
	Game.consoleOpen = false;
	return CMD_SUCCESS;
}

int Cmd_ToggleSpawn(const char *cmdstr)
{
	gEnemiesEnabled = !gEnemiesEnabled;
	if (gEnemiesEnabled)
		Console_PrintMsg("Enemy spawns enabled");
	else
		Console_PrintMsg("Enemy spawns disabled");
	return CMD_SUCCESS;
}

int Cmd_Levelup(const char *cmdstr)
{
	Player_AwardXP(&World_Players[0], LEVELXPS[World_Players[0].level + 1] - World_Players[0].xp);
	return CMD_SUCCESS;
}

int Cmd_RevealMap(const char *cmdstr)
{
	if (Game.state != GAME_STATE_PLAY && Game.state != GAME_STATE_PAUSED)
		return CMD_ERROR;
	for (int i = 0; i < LEVEL_SIZE * LEVEL_SIZE; i++)
		Level.vismap[i] = 2;
	return CMD_SUCCESS;
}

int Cmd_Give(const char *cmdstr)
{
	char *itemName = GetNextArg(cmdstr);

	if (Game.state != GAME_STATE_PLAY && Game.state != GAME_STATE_PAUSED)
		return CMD_ERROR;

	if (!itemName)
	{
		Console_PrintMsg("Must specify an item to give");
		return CMD_ERROR;
	}

	Item_t item = Item_GetByName(itemName);
	if (item.type == INVALID_ITEM_TYPE)
	{
		Console_PrintMsg("Unable to find item with type '%s'", itemName);
		return CMD_ERROR;
	}

	if (!Inv_AutoPlace(&World_Players[0].inventory, item))
	{
		World_DropItem(item, World_Players[0].entity->x, World_Players[0].entity->y, 0.6f, World_Players[0].entity->rot);
	}
	
	return CMD_SUCCESS;
}

int Cmd_Summon(const char *cmdstr)
{
	if (Game.state != GAME_STATE_PLAY && Game.state != GAME_STATE_PAUSED)
		return CMD_ERROR;
	char *entityname = GetNextArg(cmdstr);
	if (!entityname)
	{
		Console_PrintMsg("USAGE: summon [entity name]");
		return CMD_ERROR;
	}
	int entityId = Obj_EntityIdByName(entityname);
	if (entityId != -1)
	{
		float xd = Math_Sin(World_Players[0].entity->rot);
		float yd = Math_Cos(World_Players[0].entity->rot);
		World_NewObject(entityId, World_Players[0].entity->x + xd * 1.8f, World_Players[0].entity->y + yd * 1.8f);
		return CMD_SUCCESS;
	}
	return CMD_ERROR;
}

int Cmd_Register(const char *cmdstr)
{
	int i, savedi = cmdi;
	int reg = -1;
	char *val, *name = GetNextArg(cmdstr);

	struct VMContext ctx;
	ctx.obj = World_Players[0].entity;
	ctx.other = NULL;
	ctx.item = Inv_GetItem(&World_Players[0].inventory, World_Players[0].handSlot, NULL);

	if (name == NULL)
	{
		Console_PrintMsg("Must specify a register to view/change");
		return CMD_ERROR;
	}
	
	for (i = 0; i < NUM_REGISTERS; i++) {
		if (SDL_strcmp(registerNames[i], name) == 0)
		{
			reg = i;
			break;
		}
	}
	if (reg == -1)
	{
		Console_PrintMsg("Unknown register '%s'", name);
		return CMD_ERROR;
	}

	val = GetNextArg(cmdstr);
	if (val != NULL)
	{
		VM_SetRegister(reg, SDL_atoi(val), &ctx);
	}

	cmdi = savedi;
	name = GetNextArg(cmdstr);
	Console_PrintMsg("%s: %d", name, VM_GetRegister(reg, &ctx));
	return CMD_SUCCESS;
}

int Cmd_Cheat(const char *name, int bitmask)
{
	if (Game.state != GAME_STATE_PLAY && Game.state != GAME_STATE_PAUSED)
		return CMD_ERROR;
	Game.cheats ^= bitmask;
	if (Game.cheats & bitmask)
		Console_PrintMsg("%s ACTIVATED", name);
	else
		Console_PrintMsg("%s OFF", name);
	return CMD_SUCCESS;
}

int Cmd_Connect(const char *cmdstr)
{
	SDL_strlcpy(Game.serverAddr, GetNextArg(cmdstr), 63);
	Game_Start(GAME_START_JOIN, GAME_START_FLOOR);
	return CMD_SUCCESS;
}
