#include "VM.h"

#include "Game.h"
#include "Level.h"
#include "Log.h"
#include "Particle.h"
#include "Player.h"
#include "rovdefs.h"
#include "World.h"

static byte* bytecode;
static const char** stringTable;

#define VM_TOFLOAT(v) ((float)v)

typedef struct VMType
{
	enum { VMTYPE_INTEGER, VMTYPE_FLOAT } type;
	union { int i; float f; } value;
} vmtype;

#define VM_STACKSIZE 32
static vmtype stack[VM_STACKSIZE];
static int stackptr = 0;

static void LoadStringTable(size_t len)
{
	int i, stringTableOffs, numStrings;
	char* strptr;

	stringTableOffs = *(uint32_t*)(&bytecode[len - 4]);
	strptr = bytecode + stringTableOffs;
	numStrings = *(uint32_t*)(strptr);

	stringTable = SDL_calloc(numStrings, sizeof(const char*));
	strptr += sizeof(uint32_t);
	for (i = 0; i < numStrings; i++)
	{
		stringTable[i] = strptr;
		strptr += SDL_strlen(stringTable[i]) + 1;
	}
}

void VM_Init(const RPKFile* rpk)
{
	byte* data;
	size_t len;

	len = RPK_GetEntryData(rpk, "bytecode", (void**)&data);
	if (len == 0)
	{
		Msg_Fatal("Unable to read entity action bytecode");
	}

	bytecode = SDL_malloc(len);
	SDL_memcpy(bytecode, data, len);
	SDL_free(data);

	LoadStringTable(len);
}

void VM_Cleanup(void)
{
	SDL_free(bytecode);
	SDL_free(stringTable);
}

static bool InsnHasImm(byte insn)
{
	switch (insn)
	{
	case INSN_LOGI_JMP:
	case INSN_LOGI_JMPIF:
	case INSN_CORE_GET:
	case INSN_CORE_SET:
	case INSN_CORE_PUSH:
	case INSN_ACT_SETSTATE:
	case INSN_ACT_SHOOT:
	case INSN_ACT_SAY:
	case INSN_ACT_PLAYSOUND:
	case INSN_ACT_DROPITEM:
	case INSN_ACT_SUMMONBURST:
	case INSN_CND_STATEIS:
	case INSN_CND_OTHERIS:
		return true;
	default:
		return false;
	}
}

int VM_GetRegister(enum Register reg, struct VMContext* ctx)
{
	switch (reg)
	{
	case REG_GLOBAL0: return Game.globalReg[0];
	case REG_GLOBAL1: return Game.globalReg[1];
	case REG_GLOBAL2: return Game.globalReg[2];
	case REG_GLOBAL3: return Game.globalReg[3];
	case REG_FLOOR: return Game.depth;
	case REG_TIME: return Game.levelTime;
	case REG_NUMPLAYERS: return World_NumPlayers();
	case REG_XP: return ctx->player->xp;
	case REG_ENTITY_HEALTH: return ctx->obj->health;
	case REG_ENTITY_DATA:
		if (ctx->item != NULL)
			return ctx->item->data;
		return ctx->obj->data;
	case REG_ENTITY_TEAM: return ctx->obj->team;
	case REG_ITEM_MANA: return ctx->player->mana;
	case REG_ITEM_DURABILITY:
		if (ctx->item == NULL)
		{
			Msg_Warning("Non-item script is attempting to use item register 'durability'");
			break;
		}
		return ctx->item->remainingUses;
	case REG_ITEM_SPEED:
		if (ctx->item == NULL)
		{
			Msg_Warning("Non-item script is attempting to use item register 'speed'");
			break;
		}
		return (int)Item_GetStat(ctx->item, ISTAT_SPEED);
	case REG_ITEM_RANGE:
		if (ctx->item == NULL)
		{
			Msg_Warning("Non-item script is attempting to use item register 'range'");
			break;
		}
		return (int)Item_GetStat(ctx->item, ISTAT_RANGE);
	case REG_ITEM_DAMAGE:
		if (ctx->item == NULL)
		{
			Msg_Warning("Non-item script is attempting to use item register 'damage'");
			break;
		}
		return (int)Item_GetStat(ctx->item, ISTAT_DAMAGE);
	case REG_PLAYER_STR: return ctx->player->str;
	case REG_PLAYER_SPD: return ctx->player->spd;
	case REG_PLAYER_DEF: return ctx->player->def;
	case REG_PLAYER_VIT: return ctx->player->vit;
	case REG_PLAYER_INT: return ctx->player->itl;
	default:
		Msg_Warning("Attempted to get value of unknown register %d", reg);
		return 0;
	}
	return 0;
}

static void SetPlayerStat(player_t *player, enum PlayerStatType type, int value)
{
	if (value > PLAYER_STAT_CAP)
		value = PLAYER_STAT_CAP;
	if (value < 0)
		value = 0;
	switch (type)
	{
	case PLAYER_STAT_STR:
		player->str = value;
		break;
	case PLAYER_STAT_DEF:
		player->def = value;
		break;
	case PLAYER_STAT_SPD:
		player->spd = value;
		break;
	case PLAYER_STAT_VIT:
		player->vit = value;
		break;
	case PLAYER_STAT_INT:
		player->itl = value;
		player->maxMana = player->itl * PLAYER_MANA_PER_ITL;
		break;
	}
}

void VM_SetRegister(enum Register reg, int value, struct VMContext* ctx)
{
	switch (reg)
	{
	case REG_GLOBAL0:
		Game.globalReg[0] = value;
		break;
	case REG_GLOBAL1:
		Game.globalReg[1] = value;
		break;
	case REG_GLOBAL2:
		Game.globalReg[2] = value;
		break;
	case REG_GLOBAL3:
		Game.globalReg[3] = value;
		break;
	case REG_XP:
		Msg_Warning("Cannot set value of XP register, use awardxp instead");
		break;
	case REG_ENTITY_HEALTH:
		ctx->obj->health = value;
		break;
	case REG_ENTITY_DATA:
		if (ctx->item != NULL)
			ctx->item->data = value;
		else
			ctx->obj->data = value;
		break;
	case REG_ENTITY_TEAM:
		ctx->obj->team = value;
		break;
	case REG_ITEM_MANA:
		ctx->player->mana = value;
		if (ctx->player->mana < 0) ctx->player->mana = 0;
		if (ctx->player->mana > ctx->player->maxMana) ctx->player->mana = ctx->player->maxMana;
		break;
	case REG_ITEM_DURABILITY:
		if (ctx->item == NULL)
		{
			Msg_Warning("Non-item script is attempting to set value of register 'durability'");
			break;
		}
		ctx->item->remainingUses = value;
		break;
	case REG_PLAYER_STR:
		SetPlayerStat(ctx->player, PLAYER_STAT_STR, value);
		Player_ApplyStatsToEntity(ctx->player);
		break;
	case REG_PLAYER_SPD:
		SetPlayerStat(ctx->player, PLAYER_STAT_SPD, value);
		Player_ApplyStatsToEntity(ctx->player);
		break;
	case REG_PLAYER_DEF:
		SetPlayerStat(ctx->player, PLAYER_STAT_DEF, value);
		Player_ApplyStatsToEntity(ctx->player);
		break;
	case REG_PLAYER_VIT:
		SetPlayerStat(ctx->player, PLAYER_STAT_VIT, value);
		Player_ApplyStatsToEntity(ctx->player);
		break;
	case REG_PLAYER_INT:
		SetPlayerStat(ctx->player, PLAYER_STAT_INT, value);
		Player_ApplyStatsToEntity(ctx->player);
		break;
	case REG_FLOOR:
	case REG_TIME:
	case REG_NUMPLAYERS:
	case REG_ITEM_SPEED:
	case REG_ITEM_RANGE:
	case REG_ITEM_DAMAGE:
		Msg_Warning("Attempted to set value of read-only register %s", registerNames[reg]);
		break;
	case NUM_REGISTERS:
		Msg_Warning("Attempted to set value of invalid register");
		break;
	}
}

void VM_Push(vmtype v)
{
	stack[stackptr++] = v;
	if (stackptr >= VM_STACKSIZE)
		Msg_Warning("Stack overflow!");
}

static const vmtype errResult = { VMTYPE_INTEGER, {-1} };
vmtype VM_Pop(void)
{
	if (stackptr <= 0)
	{
		Msg_Warning("Stack underflow!");
		return errResult;
	}
	return stack[--stackptr];
}

void VM_PushInt(int v)
{
	vmtype pushv = { VMTYPE_INTEGER, {v} };
	VM_Push(pushv);
}

int VM_PopInt(void)
{
	vmtype res = VM_Pop();
	if (res.type == VMTYPE_INTEGER)
		return res.value.i;
	else
		return (int)res.value.f;
}

void VM_PushFloat(float v)
{
	vmtype pushv = { .type = VMTYPE_FLOAT, .value.f = v };
	VM_Push(pushv);
}

float VM_PopFloat(void)
{
	vmtype res = VM_Pop();
	if (res.type == VMTYPE_INTEGER)
		return (float)res.value.i;
	else
		return res.value.f;
}

int VM_RunBytecode(size_t start, struct VMContext* ctx)
{
	byte insn;
	size_t pc = start;
	vmtype imm;

	int actionCount = 0;

	Obj_t* obj = ctx->obj;

	do
	{
		insn = bytecode[pc++];
		imm.type = VMTYPE_INTEGER;
		imm.value.i = 0;

		if (InsnHasImm(insn))
		{
			enum ImmType type = bytecode[pc++];
			switch (type)
			{
			case IMMTYPE_U8:
				imm.value.i = bytecode[pc++];
				break;
			case IMMTYPE_S8:
				imm.value.i = ((char*)bytecode)[pc++];
				break;
			case IMMTYPE_U16:
				imm.value.i = *(uint16_t*)(bytecode + pc);
				pc += 2;
				break;
			case IMMTYPE_S16:
				imm.value.i = *(int16_t*)(bytecode + pc);
				pc += 2;
				break;
			case IMMTYPE_U32:
				imm.value.i = *(uint32_t*)(bytecode + pc);
				pc += 4;
				break;
			case IMMTYPE_S32:
				imm.value.i = *(int32_t*)(bytecode + pc);
				pc += 4;
				break;
			case IMMTYPE_FLOAT32:
				imm.type = VMTYPE_FLOAT;
				imm.value.f = (*(float*)(bytecode + pc));
				pc += 4;
				break;
			}
		}

		switch (insn)
		{
		case INSN_LOGI_ENDCODE:
			break;
		case INSN_LOGI_NOT:
			VM_PushInt(!VM_PopInt());
			break;
		case INSN_LOGI_AND: {
			int a = VM_PopInt();
			int b = VM_PopInt();
			VM_PushInt(a && b);
			break;
		}
		case INSN_LOGI_OR: {
			int a = VM_PopInt();
			int b = VM_PopInt();
			VM_PushInt(a || b);
			break;
		}
		case INSN_LOGI_JMP:
			pc = imm.value.i;
			insn = bytecode[pc];
			break;
		case INSN_LOGI_JMPIF:
			if (VM_PopInt())
			{
				pc = imm.value.i;
				insn = bytecode[pc];
			}
			break;
		case INSN_CORE_GET:
			VM_PushInt(VM_GetRegister(imm.value.i, ctx));
			break;
		case INSN_CORE_SET:
			VM_SetRegister(imm.value.i, VM_PopInt(), ctx);
			break;
		case INSN_CORE_ADD:
			VM_PushInt(VM_PopInt() + VM_PopInt());
			break;
		case INSN_CORE_SUB:
			VM_PushInt(VM_PopInt() - VM_PopInt());
			break;
		case INSN_CORE_EQUALS:
			VM_PushInt(VM_PopInt() == VM_PopInt());
			break;
		case INSN_CORE_LESS:
			VM_PushInt(VM_PopInt() < VM_PopInt());
			break;
		case INSN_CORE_GREATER:
			VM_PushInt(VM_PopInt() > VM_PopInt());
			break;
		case INSN_CORE_LESSEQU:
			VM_PushInt(VM_PopInt() <= VM_PopInt());
			break;
		case INSN_CORE_GREATEREQU:
			VM_PushInt(VM_PopInt() >= VM_PopInt());
			break;
		case INSN_CORE_PUSH:
			VM_Push(imm);
			break;
		case INSN_CORE_POP:
			VM_Pop();
			break;
		case INSN_ACT_SETSTATE:
			Obj_SetState(obj, imm.value.i);
			break;
		case INSN_ACT_NEXTSTATE:
			Obj_SetState(obj, obj->stateId + 1);
			break;
		case INSN_ACT_STOPMOVING:
			obj->xd = 0.0f;
			obj->yd = 0.0f;
			break;
		case INSN_ACT_DELETEME:
			World_RemoveObject(obj);
			actionCount++;
			break;
		case INSN_ACT_MOVE: {
			float angle = DEG_TO_RAD((float)VM_PopInt());
			float speed = VM_PopFloat();
			float accel = VM_PopFloat();
			float xa, ya;
			AI_CalcEnemyDirVector(obj, &xa, &ya);
			if (angle != 0.0f)
				AI_OffsetDirVector(&xa, &ya, angle);
			AI_SetMovingDir(obj, speed, accel, xa, ya);
			actionCount++;
			break;
		}
		case INSN_ACT_MOVERANDOM: {
			float speed = VM_PopFloat();
			float accel = VM_PopFloat();
			float direction = (Math_RandomFloat() * 2.0f - 1.0f) * MATH_PI * 2;
			AI_SetMovingDir(obj, speed, accel, Math_Sin(direction), Math_Cos(direction));
			actionCount++;
			break;
		}
		case INSN_ACT_MOVEALLY: {
			float angle = DEG_TO_RAD((float)VM_PopInt());
			float speed = VM_PopFloat();
			float accel = VM_PopFloat();
			float xa, ya;
			AI_CalcAllyDirVector(obj, &xa, &ya);
			if (angle != 0.0f)
				AI_OffsetDirVector(&xa, &ya, angle);
			AI_SetMovingDir(obj, speed, accel, xa, ya);
			actionCount++;
			break;
		}
		case INSN_ACT_HITSCAN: {
			int damage = VM_PopInt();
			int maxHits = VM_PopInt();
			float range = VM_PopFloat();
			damage += Obj_GetModifierValue(obj, STATMOD_STRENGTH);
			actionCount += AI_HitscanAttack(obj, damage, maxHits, ctx->obj->rot, range, ctx->item != NULL);
			break;
		}
		case INSN_ACT_HIT_SWEEP: {
			float range = VM_PopFloat();
			int damage = VM_PopInt();
			damage += Obj_GetModifierValue(obj, STATMOD_STRENGTH);
			actionCount += AI_SweepHit(obj, range, damage, ctx->item != NULL);
			break;
		}
		case INSN_ACT_HIT_RADIUS: {
			float range = VM_PopFloat();
			int damage = VM_PopInt();
			damage += Obj_GetModifierValue(obj, STATMOD_STRENGTH);
			actionCount += AI_RadiusHit(obj, range, damage, ctx->item != NULL);
			break;
		}
		case INSN_ACT_SHOOT: {
			int data = VM_PopInt();
			float speed = VM_PopFloat();
			float angle = DEG_TO_RAD((float)VM_PopInt());
			AI_FireProjectile(obj, imm.value.i, speed, data, angle);
			actionCount++;
			break;
		}
		case INSN_ACT_PLAYSOUND: {
			int soundId = Audio_GetIDForName(stringTable[imm.value.i]);
			World_PlayEntitySound(obj, soundId);
			break;
		}
		case INSN_ACT_HURTOTHER:
			if (ctx->other == NULL)
				Msg_Warning("Attempted to hurt other entity in function that doesn't have a other entity");
			else
			{
				Obj_Hurt(ctx->other, obj, VM_PopInt());
				actionCount++;
			}
			break;
		case INSN_ACT_DELETEOTHER:
			if (ctx->other == NULL)
				Msg_Warning("Attempted to delete other entity in function that doesn't have a other entity");
			else
			{
				World_RemoveObject(ctx->other);
				actionCount++;
			}
			break;
		case INSN_ACT_THROWOTHER:
			if (ctx->other == NULL)
				Msg_Warning("Attempted to throw other entity in function that doesn't have an other entity");
			else
			{
				float angle = Math_RandomFloat() * MATH_PI * 2.0f;
				Obj_Knockback(ctx->other, ctx->other->x + Math_Sin(angle), ctx->other->y + Math_Cos(angle), VM_PopInt());
				actionCount++;
			}
			break;
		case INSN_ACT_STATMODOTHER:
			if (ctx->other == NULL)
				Msg_Warning("Attempted to throw other entity in function that doesn't have an other entity");
			else
			{
				int target = VM_PopInt();
				float modifier = VM_PopFloat();
				int duration = VM_PopInt();
				Obj_AddStatModifier(ctx->other, target, modifier, duration);
				actionCount++;
			}
			break;
		case INSN_ACT_DROPITEM: {
			float xd, yd;
			Item_t item;

			xd = ctx->player->entity->x - obj->x;
			yd = ctx->player->entity->y - obj->y;

			float dist = Math_Sqrt(xd * xd + yd * yd);
			xd /= dist;
			yd /= dist;

			item = Item_GetById(imm.value.i);
			World_DropItem(item, obj->x, obj->y, 0.4f, Math_Atan2(yd, xd));
			actionCount++;
			break;
		}
		case INSN_ACT_DROPLOOT: {
			float xd, yd;
			Item_t loot;

			xd = ctx->player->entity->x - obj->x;
			yd = ctx->player->entity->y - obj->y;

			float dist = Math_Sqrt(xd * xd + yd * yd);
			xd /= dist;
			yd /= dist;

			loot = Loot_PickRandom(Game.depth);
			World_DropItem(loot, obj->x, obj->y, 0.4f, Math_Atan2(xd, yd));
			actionCount++;
			break;
		}
		case INSN_ACT_HEAL:
			Obj_Heal(obj, VM_PopInt());
			actionCount++;
			break;
		case INSN_ACT_WARP:
			Game_ChangeFloor(VM_PopInt(), -1, -1);
			break;
		case INSN_ACT_WARPTO: {
			int floor = VM_PopInt();
			float x = VM_PopFloat();
			float y = VM_PopFloat();
			Game_ChangeFloor(floor, x, y);
			break;
		}
		case INSN_ACT_AWARDXP:
			Player_AwardXP(ctx->player, VM_PopInt());
			break;
		case INSN_ACT_PARTICLEBURST: {
			int count = VM_PopInt();
			float power = VM_PopFloat();
			int colour = VM_PopInt();
			particleBurst(obj->x, obj->y, obj->z + 0.5f, obj->xd, obj->yd, obj->zd + power, colour, count);
			break;
		}
		case INSN_ACT_SAY: {
			Game_ShowStatusMsg(stringTable[imm.value.i]);
			break;
		}
		case INSN_ACT_ADDSTATMOD: {
			int target = VM_PopInt();
			float modifier = VM_PopFloat();
			int duration = VM_PopInt();
			Obj_AddStatModifier(obj, target, modifier, duration);
			actionCount++;
			break;
		}
		case INSN_ACT_SUMMONBURST: {
			int count = VM_PopInt();
			float radius = VM_PopFloat();
			int data = VM_PopInt();
			AI_SummonBurst(obj, imm.value.i, count, radius, data);
			actionCount++;
			break;
		}
		case INSN_CND_CHANCE:
			VM_PushInt((RNG_CHANCE(VM_PopInt())));
			break;
		case INSN_CND_SEESENEMY: {
			VM_PushInt(AI_CanSeeAnEnemy(obj));
			break;
		}
		case INSN_CND_ENEMYWITHIN: {
			float xd, yd;
			float range = VM_PopFloat();
			Obj_t* enemyObj;

			enemyObj = World_GetNearestEnemyObj(obj->team, obj->x, obj->y);
			if (enemyObj == NULL)
			{
				VM_PushInt(false);
				break;
			}

			xd = enemyObj->x - obj->x;
			yd = enemyObj->y - obj->y;

			VM_PushInt(Math_Abs(xd * xd + yd * yd) < (range * range));
			break;
		}
		case INSN_CND_SEESALLY: {
			VM_PushInt(AI_CanSeeAnAlly(obj));
			break;
		}
		case INSN_CND_ALLYWITHIN: {
			float xd, yd;
			float range = VM_PopFloat();
			Obj_t* allyObj;

			allyObj = World_GetNearestAllyObj(obj, obj->team, obj->x, obj->y);
			if (allyObj == NULL)
			{
				VM_PushInt(false);
				break;
			}

			xd = allyObj->x - obj->x;
			yd = allyObj->y - obj->y;

			VM_PushInt(Math_Abs(xd * xd + yd * yd) < (range * range));
			break;
		}
		case INSN_CND_STATEIS:
			VM_PushInt(obj->stateId == imm.value.i);
			break;
		case INSN_CND_OTHERIS:
			if (ctx->other == NULL)
			{
				Msg_Warning("Attempted to check other entity type in function that doesn't have an other entity");
				VM_PushInt(false);
				break;
			}
			VM_PushInt(ctx->other->typeId == imm.value.i);
			break;
		default:
			Msg_Warning("Encountered unknown bytecode op %d\n", insn);
			break;
		}
	} while (insn != INSN_LOGI_ENDCODE);
	if (stackptr != 0)
	{
		Msg_Warning("Stack imbalance from script %d", start);
		stackptr = 0;
	}
	return actionCount > 0;
}
