#include <SDL.h>

#include "Game.h"
#include "Inventory.h"
#include "Item.h"
#include "Level.h"
#include "LevelGen.h"
#include "Log.h"
#include "Render.h" // for item sprites
#include "System.h"
#include "World.h"

// Needs to handle:
// - Floors out of order
// - Dynamic # of entities, or not
// - Detecting incompatibility with RPK file
// Save file structure:
//  Number of floors
//  Floor offsets[numfloors] - offset of -1 if that floor isn't saved
//  Player data
//  Inventory

// World.c
extern Obj_t *objects;
extern unsigned char *hasobj;

struct ItemSprite
{
	Item_t item;
	float x, y, z;
	float xa, ya, za;
};
extern struct ItemSprite wlditems[WORLD_MAX_ITEMS];
extern unsigned char hasitem[WORLD_MAX_ITEMS];

// Entity.c
extern struct ObjType *TYPES;
extern struct ObjState *STATES;

static void WriteEntity(Obj_t *obj, SDL_RWops *io);
static void ReadEntity(Obj_t *obj, SDL_RWops *io);
static void WriteInventory(SDL_RWops *io);
static void ReadInventory(SDL_RWops *io);

static void WriteCurrentFloor(SDL_RWops *io)
{
	int i;

	Msg_Info("Writing floor %d to save file...", Game.depth);

	// TODO: Level properties shouldn't need to be saved
	// Only the tilemap, vismap and time need to be saved and loaded.
	// The rest can be read from the levelgen file.
	io->write(io, &Level.wallHeight, 1, 1);
	io->write(io, &Level.ambientBrightness, 1, 1);
	io->write(io, &Level.fogDepth, 1, 1);
	io->write(io, &Level.tileset, 1, 1);
	io->write(io, &Level.flags, 1, 1);
	io->write(io, &Game.levelTime, 4, 1);
	io->write(io, Level.tilemap, LEVEL_SIZE * LEVEL_SIZE, 1);
	io->write(io, Level.vismap, LEVEL_SIZE * LEVEL_SIZE, 1);
	io->write(io, Level.bossName, 1, 32);
	io->write(io, Level.songName, 1, 32);
	io->write(io, Level.enterName, 1, 64);
	io->write(io, &Level.bossColour, 4, 1);
	io->write(io, &Level.fogColour, 4, 1);
	io->write(io, &Level.ambientParticleColour, 4, 1);
	io->write(io, &Level.ambientParticleSpeed, 4, 1);

	int numObjs = 0;
	for (i = 0; i < WORLD_MAX_ENTITIES; i++)
	{
		if (hasobj[i] && &objects[i] != World_Players[0].entity)
			numObjs++;
	}

	io->write(io, &numObjs, sizeof(int), 1);

	for (i = 0; i < WORLD_MAX_ENTITIES; i++)
	{
		if (!hasobj[i])
			continue;
		Obj_t *obj = &objects[i];
		if (obj == World_Players[0].entity)
			continue;
		WriteEntity(obj, io);
	}

	int numItems = 0;
	for (i = 0; i < WORLD_MAX_ITEMS; i++)
	{
		if (hasitem[i])
			numItems++;
	}

	io->write(io, &numItems, sizeof(int), 1);

	for (i = 0; i < WORLD_MAX_ITEMS; i++)
	{
		if (hasitem[i])
		{
			io->write(io, &wlditems[i].x, sizeof(float), 1);
			io->write(io, &wlditems[i].y, sizeof(float), 1);
			io->write(io, &wlditems[i].item.type, 1, 1);
			io->write(io, &wlditems[i].item.stats, sizeof(wlditems[i].item.stats), 1);
			io->write(io, &wlditems[i].item.remainingUses, sizeof(int), 1);
		}
	}
}

static void ReadFloor(SDL_RWops *io, int lvlDepth)
{
	int i;

	Msg_Info("Reading floor %d from save file...", lvlDepth);

	io->read(io, &Level.wallHeight, 1, 1);
	io->read(io, &Level.ambientBrightness, 1, 1);
	io->read(io, &Level.fogDepth, 1, 1);
	io->read(io, &Level.tileset, 1, 1);
	io->read(io, &Level.flags, 1, 1);
	io->read(io, &Game.levelTime, 4, 1);
	io->read(io, Level.tilemap, LEVEL_SIZE * LEVEL_SIZE, 1);
	io->read(io, Level.vismap, LEVEL_SIZE * LEVEL_SIZE, 1);
	io->read(io, Level.bossName, 1, 32);
	io->read(io, Level.songName, 1, 32);
	io->read(io, Level.enterName, 1, 64);
	io->read(io, &Level.bossColour, 4, 1);
	io->read(io, &Level.fogColour, 4, 1);
	io->read(io, &Level.ambientParticleColour, 4, 1);
	io->read(io, &Level.ambientParticleSpeed, 4, 1);

	Level.skybox = LvlGen_GetSkyboxForFloor(lvlDepth);

	int numObjs = 0;
	io->read(io, &numObjs, sizeof(int), 1);

	for (i = 0; i < numObjs; i++)
	{
		Obj_t *obj = &objects[i];
		ReadEntity(obj, io);
		obj->id = i;
		if (obj->team == ENTITY_TEAM_BOSS && obj->health > 0)
			Level.boss = obj;
		hasobj[i] = true;
		World_SpriteFromEntity(obj, &World_Sprites[i]);
	}

	int numItems = 0;
	io->read(io, &numItems, sizeof(int), 1);
	for (i = 0; i < numItems; i++)
	{
		io->read(io, &wlditems[i].x, sizeof(float), 1);
		io->read(io, &wlditems[i].y, sizeof(float), 1);

		io->read(io, &wlditems[i].item.type, 1, 1);
		io->read(io, &wlditems[i].item.stats, sizeof(wlditems[i].item.stats), 1);
		io->read(io, &wlditems[i].item.remainingUses, sizeof(int), 1);

		hasitem[i] = true;

		World_Sprites[i + WORLD_ITEM_BASEID].id = i + WORLD_ITEM_BASEID;
		World_Sprites[i + WORLD_ITEM_BASEID].x = wlditems[i].x;
		World_Sprites[i + WORLD_ITEM_BASEID].y = wlditems[i].y;
		World_Sprites[i + WORLD_ITEM_BASEID].z = wlditems[i].z;
		World_Sprites[i + WORLD_ITEM_BASEID].sprite = ItemTypes[wlditems[i].item.type].worldSprite;
		World_Sprites[i + WORLD_ITEM_BASEID].fx = SPRITEFX_NONE;
		World_Sprites[i + WORLD_ITEM_BASEID].angle = 0;
	}
}

static void WritePlayer(SDL_RWops *io)
{
	Msg_Info("Writing player data to save file...");
	io->write(io, &World_Players[0].entity->x, sizeof(float), 1);
	io->write(io, &World_Players[0].entity->y, sizeof(float), 1);
	io->write(io, &World_Players[0].entity->health, sizeof(int), 1);
	io->write(io, &World_Players[0].entity->modifiers, sizeof(World_Players[0].entity->modifiers), 1);
	io->write(io, &World_Players[0].entity->rot, sizeof(float), 1);
	io->write(io, &World_Players[0].manaTimer, sizeof(float), 1);
	io->write(io, &World_Players[0].deathStatistics, sizeof(World_Players[0].deathStatistics), 1);
	SDL_WriteU8(io, World_Players[0].str);
	SDL_WriteU8(io, World_Players[0].spd);
	SDL_WriteU8(io, World_Players[0].def);
	SDL_WriteU8(io, World_Players[0].vit);
	SDL_WriteU8(io, World_Players[0].itl);
	SDL_WriteLE16(io, World_Players[0].mana);
	SDL_WriteLE16(io, World_Players[0].maxMana);
	io->write(io, &World_Players[0].handSlot, 1, 1);
	io->write(io, &World_Players[0].statPoints, 1, 1);
	io->write(io, &World_Players[0].level, 1, 1);
	io->write(io, &World_Players[0].xp, sizeof(int), 1);
	io->write(io, &Game.depth, 1, 1);
	io->write(io, &Game.globalReg, sizeof(int), 4);
}

static void ReadPlayer(SDL_RWops *io)
{
	Msg_Info("Reading player data from save file...");
	io->read(io, &World_Players[0].entity->x, sizeof(float), 1);
	io->read(io, &World_Players[0].entity->y, sizeof(float), 1);
	io->read(io, &World_Players[0].entity->health, sizeof(int), 1);
	io->read(io, &World_Players[0].entity->modifiers, sizeof(World_Players[0].entity->modifiers), 1);
	io->read(io, &World_Players[0].entity->rot, sizeof(float), 1);
	io->read(io, &World_Players[0].manaTimer, sizeof(float), 1);
	io->read(io, &World_Players[0].deathStatistics, sizeof(World_Players[0].deathStatistics), 1);
	World_Players[0].str = SDL_ReadU8(io);
	World_Players[0].spd = SDL_ReadU8(io);
	World_Players[0].def = SDL_ReadU8(io);
	World_Players[0].vit = SDL_ReadU8(io);
	World_Players[0].itl = SDL_ReadU8(io);
	World_Players[0].mana = SDL_ReadLE16(io);
	World_Players[0].maxMana = SDL_ReadLE16(io);
	io->read(io, &World_Players[0].handSlot, 1, 1);
	io->read(io, &World_Players[0].statPoints, 1, 1);
	io->read(io, &World_Players[0].level, 1, 1);
	io->read(io, &World_Players[0].xp, sizeof(int), 1);
	io->read(io, &Game.depth, 1, 1);
	io->read(io, &Game.globalReg, sizeof(int), 4);
}

static void WriteEntity(Obj_t *obj, SDL_RWops *io)
{
	SDL_WriteLE16(io, obj->id);
	SDL_WriteLE16(io, obj->data);
	SDL_WriteLE16(io, obj->health);
	SDL_WriteLE16(io, obj->maxHealth);
	SDL_RWwrite(io, &obj->x, sizeof(float), 1);
	SDL_RWwrite(io, &obj->y, sizeof(float), 1);
	SDL_RWwrite(io, &obj->z, sizeof(float), 1);
	SDL_RWwrite(io, &obj->xd, sizeof(float), 1);
	SDL_RWwrite(io, &obj->yd, sizeof(float), 1);
	SDL_RWwrite(io, &obj->zd, sizeof(float), 1);
	SDL_RWwrite(io, &obj->accel, sizeof(float), 1);
	SDL_WriteLE32(io, obj->invulnerableTime);
	SDL_WriteLE32(io, obj->stateTimer);
	SDL_WriteU8(io, obj->defense);
	SDL_WriteU8(io, obj->team);
	SDL_WriteLE16(io, obj->stateId);
	SDL_WriteLE16(io, obj->typeId);
	SDL_RWwrite(io, &obj->modifiers, sizeof(obj->modifiers), 1);
	// NOTE: The owner of the entity is not saved.
}

static void ReadEntity(Obj_t *obj, SDL_RWops *io)
{
	obj->id = SDL_ReadLE16(io);
	obj->data = SDL_ReadLE16(io);
	obj->health = SDL_ReadLE16(io);
	obj->maxHealth = SDL_ReadLE16(io);
	SDL_RWread(io, &obj->x, sizeof(float), 1);
	SDL_RWread(io, &obj->y, sizeof(float), 1);
	SDL_RWread(io, &obj->z, sizeof(float), 1);
	SDL_RWread(io, &obj->xd, sizeof(float), 1);
	SDL_RWread(io, &obj->yd, sizeof(float), 1);
	SDL_RWread(io, &obj->zd, sizeof(float), 1);
	SDL_RWread(io, &obj->accel, sizeof(float), 1);
	obj->invulnerableTime = SDL_ReadLE32(io);
	obj->stateTimer = SDL_ReadLE32(io);
	obj->defense = SDL_ReadU8(io);
	obj->team = SDL_ReadU8(io);
	obj->stateId = SDL_ReadLE16(io);
	obj->typeId = SDL_ReadLE16(io);
	SDL_RWread(io, &obj->modifiers, sizeof(obj->modifiers), 1);
	// NOTE: The owner of the entity is not loaded, it could maybe be reconstructed with the id?

	obj->state = &STATES[obj->stateId];
	obj->type = &TYPES[obj->typeId];
}

static void WriteInventory(SDL_RWops *io)
{
	inventory_t *inv = &World_Players[0].inventory;
	const int dummy = INVALID_ITEM_TYPE;
	int i;
	for (i = 0; i < INV_SIZE; i++)
	{
		if (inv->slots[i] & SLOT_CONTAINSITEM)
		{
			io->write(io, &inv->items[i].type, 2, 1);
			io->write(io, &inv->items[i].data, 2, 1);
			io->write(io, &inv->items[i].stats, sizeof(inv->items[i].stats), 1);
			io->write(io, &inv->items[i].remainingUses, sizeof(int), 1);
		}
		else
		{
			io->write(io, &dummy, 2, 1);
		}
	}
}

static void ReadInventory(SDL_RWops *io)
{
	inventory_t *inv = &World_Players[0].inventory;
	unsigned short itemType;
	int i;

	Inv_Clear(inv);

	for (i = 0; i < INV_SIZE; i++)
	{
		io->read(io, &itemType, 2, 1);
		if (itemType != INVALID_ITEM_TYPE)
		{
			Item_t item;
			item.type = itemType;
			io->read(io, &item.data, 2, 1);
			io->read(io, &item.stats, sizeof(inv->items[i].stats), 1);
			io->read(io, &item.remainingUses, sizeof(int), 1);
			Inv_PutItem(inv, item, i);
		}
	}
}

#define LVL_PATHBUF_SIZE 32
static char pathbuf[LVL_PATHBUF_SIZE];

void Save_SaveGame(void)
{
	SDL_RWops *io;

	Msg_Info("Save_SaveGame: saving level %d and player", Game.depth);

	SDL_snprintf(pathbuf, LVL_PATHBUF_SIZE, "saves/level%02d", Game.depth);

	OS_MkdirIfNotExists("saves");
	io = SDL_RWFromFile(pathbuf, "wb");
	if (io == NULL)
	{
		Msg_Warning("Failed to save: (%s) %s\n", pathbuf, SDL_GetError());
		return;
	}

	WriteCurrentFloor(io);
	SDL_RWclose(io);

	io = SDL_RWFromFile("saves/player", "wb");
	if (io == NULL)
	{
		Msg_Warning("Failed to save: (%s) %s\n", "player", SDL_GetError());
		return;
	}

	WritePlayer(io);
	WriteInventory(io);

	SDL_RWclose(io);
}

int Save_LoadGame(void)
{
	SDL_RWops *playerfile, *levelfile;

	playerfile = SDL_RWFromFile("saves/player", "rb");
	if (playerfile == NULL)
		return false;

	// HACK: Load into a temporary object so that the depth is loaded from the player file
	Obj_t tmpsuck;
	World_Players[0].entity = &tmpsuck;
	ReadPlayer(playerfile);
	
	Msg_Info("Save_LoadGame: Loaded player at floor %d", Game.depth);

	SDL_snprintf(pathbuf, LVL_PATHBUF_SIZE, "saves/level%02d", Game.depth);
	levelfile = SDL_RWFromFile(pathbuf, "rb");
	if (levelfile == NULL)
		return 0;
	ReadFloor(levelfile, Game.depth);
	
	World_Players[0].entity = World_NewObject(Player_EntityTypeId, 0.0f, 0.0f);
	World_Players[0].sprite = &World_Sprites[World_Players[0].entity->id];
	
	SDL_RWseek(playerfile, 0, RW_SEEK_SET);
	ReadPlayer(playerfile);
	ReadInventory(playerfile);

	SDL_RWclose(levelfile);
	SDL_RWclose(playerfile);

	return 1;
}

int Save_LoadLevel(int depth)
{
	SDL_RWops *io;
	SDL_snprintf(pathbuf, LVL_PATHBUF_SIZE, "saves/level%02d", depth);
	io = SDL_RWFromFile(pathbuf, "rb");
	if (io == NULL)
		return 0;
	Msg_Info("Save_LoadLevel: loading single floor %d", Game.depth);
	ReadFloor(io, depth);
	return 1;
}

void Save_RemoveSaves(void)
{
	Msg_Info("Save_RemoveSaves: Deleting save files");
	OS_DeleteAllInDir("saves");
}
