#include "lvlgen.hpp"

#include <cassert>
#include <iostream>
#include <sstream>
#include <fstream>
#include <stack>
#include <unordered_map>

#include "bytebuf.hpp"
#include "error.hpp"

extern std::filesystem::path gModPath;
extern std::unordered_map<std::string, int> gReferenceIds;

std::vector<LVLGENBADBlock> ParseFloorgenFile(const std::string &filename)
{
	std::vector<LVLGENBADBlock> blocks;
	LVLGENBADBlock *currentBlock = NULL;
	std::stack<std::string> tokenStack;

	std::ifstream infile(filename);
	if (!infile)
	{
		std::cerr << "Unable to read file " << filename << std::endl;
		return blocks;
	}

	std::string line;
	int lineNum = 0;
	while (std::getline(infile, line))
	{
		std::istringstream iss(line);
		std::string token;

		lineNum++;
		if (line.empty() || line[0] == '#')
			continue;

		while (iss >> token)
		{
			//std::cout << "Got token " << token << std::endl;
			if (token == "{") // Beginning of a block
			{
				LVLGENBADBlock newBlock;
				// If there are two strings on the stack, the layer has a name.
				// Otherwise, it's an unnamed block with just a type
				if (tokenStack.size() == 2)
				{
					newBlock.name = tokenStack.top();
					tokenStack.pop();
				}
				newBlock.type = tokenStack.top();
				newBlock.filepos = { filename, lineNum };
				tokenStack.pop();

				if (!currentBlock)
				{
					newBlock.parent = NULL;
					blocks.push_back(newBlock);
					currentBlock = &blocks[blocks.size() - 1];
				}
				else
				{
					newBlock.parent = currentBlock;
					currentBlock->subBlocks.push_back(newBlock);
					currentBlock = &currentBlock->subBlocks[currentBlock->subBlocks.size() - 1];
				}
			}
			else if (token == "=")
			{
				// Immediately read the next token
				// NOTE: This means that 'name = value' properties must be all on one line
				std::string value;
				iss >> std::quoted(value);

				assert(currentBlock);
				currentBlock->properties[tokenStack.top()] = value;
				tokenStack.pop();
			}
			else if (token == "}")
			{
				if (currentBlock)
				{
					currentBlock = currentBlock->parent;
				}
			}
			else
			{
				tokenStack.push(token);
			}
		}
	}

	infile.close();
	return blocks;
}

#define FLOORCONF_SIZE 188

static int flag_for_name(const std::string &name)
{
	if (name == "doors")
		return (1 << 0);
	else if (name == "lanterns")
		return (1 << 1);
	else if (name == "chests")
		return (1 << 2);
	else if (name == "mimics")
		return (1 << 3);
	else
		return 0;
}
static int IntifyFlagString(const std::string &str)
{
	int result, flag;
	char operation;
	size_t op_index, offs = 0;
	std::string flag_name;

	op_index = str.find_first_of("+-");
	flag_name = str.substr(0, op_index);
	result = flag_for_name(flag_name);

	while (op_index != std::string::npos)
	{
		operation = str[op_index];

		offs = op_index + 1;
		op_index = str.find_first_of("+-", offs);

		flag_name = str.substr(offs, op_index - offs);
		flag = flag_for_name(flag_name);

		switch (operation)
		{
		case '+':
			result |= flag;
			break;
		case '-':
			result &= ~flag;
			break;
		default:
			break;
		}
	}

	return result;
}

struct TemplateEntity
{
	std::string entityName;
	float x, y;
};
struct Template
{
	int id;
	int width, height;
	std::string tiles;
	std::vector<TemplateEntity> entities;
};
static std::map<std::string, Template> templates;
static int templateIdCounter = 0;

static Template LoadTemplate(const std::string &name)
{
	Template result;

	result.id = -1;
	result.width = -1;
	result.height = 0;

	std::filesystem::path filePath = gModPath / "levelgen" / (name + ".txt");
	std::ifstream infile(filePath);
	if (!infile)
	{
		std::cerr << "Could not read template from " << filePath << std::endl;
		return result;
	}

	std::string line;
	int lineNum = 1;
	while (std::getline(infile, line))
	{
		if (line.empty())
			continue;
		if (line[0] == '@')
		{
			std::stringstream sstr(line.substr(1));

			TemplateEntity entity;
			sstr >> entity.entityName >> entity.x >> entity.y;
			result.entities.push_back(entity);

			continue;
		}
		if (line.length() != result.width)
		{
			if (result.width == -1)
				result.width = line.length();
			else
				PrintErrorMsg({ filePath.string(), lineNum}, ERR_LVL_ERROR, "Row width doesn't match the others");
		}
		result.tiles.append(line);
		result.height++;

		lineNum++;
	}

	return result;
}

static int GetTemplateId(const std::string &name)
{
	if (templates.find(name) != templates.end())
		return templates.at(name).id;
	Template loaded = LoadTemplate(name);
	loaded.id = templateIdCounter++;
	templates[name] = loaded;
	return loaded.id;
}

static void CompileLayers(const LVLGENBADBlock &layersBlock, ByteBuffer &bytebuf)
{
	for (const LVLGENBADBlock &layer : layersBlock.subBlocks)
	{
		if (layer.type == "Fill")
		{
			char fillTile = layer.properties.at("Tile")[0];

			bytebuf.WriteUInt8(0);

			bytebuf.WriteInt8(fillTile);
		}
		else if (layer.type == "Cavern")
		{
			int density = std::stoi(layer.properties.at("Density"));
			int smoothing = std::stoi(layer.properties.at("Smoothing"));

			bytebuf.WriteUInt8(1);

			bytebuf.WriteUInt8(density);
			bytebuf.WriteUInt8(smoothing);
		}
		else if (layer.type == "Rivers")
		{
			float scale = std::stof(layer.properties.at("Scale"));
			float width = std::stof(layer.properties.at("Width"));
			float level = std::stof(layer.properties.at("Level"));
			char tile = layer.properties.at("Tile")[0];

			bytebuf.WriteUInt8(2);

			bytebuf.WriteFloat(scale);
			bytebuf.WriteFloat(width);
			bytebuf.WriteFloat(level);
			bytebuf.WriteInt8(tile);
		}
		else if (layer.type == "Rooms")
		{
			int count = std::stoi(layer.properties.at("Count"));
			int flags = IntifyFlagString(layer.properties.at("Flags"));
			char tile = layer.properties.at("GroundTile")[0];

			bytebuf.WriteUInt8(3);

			bytebuf.WriteUInt16(flags);
			bytebuf.WriteUInt8(count);
			bytebuf.WriteUInt8(tile);
		}
		else if (layer.type == "Template")
		{
			auto templateName = layer.properties.at("Name");
			int posX = std::stoi(layer.properties.at("PosX"));
			int posY = std::stoi(layer.properties.at("PosY"));

			bytebuf.WriteUInt8(4);

			bytebuf.WriteUInt32(GetTemplateId(templateName));
			bytebuf.WriteUInt16(posX);
			bytebuf.WriteUInt16(posY);
		}
	}
}

static ByteBuffer enemiesListBuf;
static int enemiesListCounter = 0;

static int CompileEnemiesList(const std::string &list)
{
	std::stringstream sstr(list);
	int id = enemiesListCounter;
	while (!sstr.eof())
	{
		int amount;
		std::string name;

		sstr >> amount >> name;

		enemiesListBuf.WriteInt16(amount);
		enemiesListBuf.WriteUInt16(gReferenceIds[name]);
		enemiesListCounter++;
	}
	enemiesListBuf.WriteInt16(-1);
	enemiesListBuf.WriteInt16(-1);
	enemiesListCounter++;
	return id;
}

void CompileFloorgen(const std::vector<LVLGENBADBlock> &blocks, const std::filesystem::path &buildDir)
{
	ByteBuffer layersBuf;
	ByteBuffer bytebuf;
	std::map<std::string, int> layerIndexByName;
	std::map<std::string, int> numLayersByName;
	int numLayers = 0;

	const LVLGENBADBlock *floorsBlock = nullptr;
	for (const LVLGENBADBlock &b : blocks)
	{
		if (b.type != ".FLOORS")
			continue;
		floorsBlock = &b;
	}
	if (floorsBlock == nullptr)
	{
		std::cerr << "No .FLOORS block!" << std::endl;
		return;
	}

	int headerSize = FLOORCONF_SIZE * floorsBlock->subBlocks.size() + 12;

	for (const LVLGENBADBlock &b : blocks)
	{
		if (b.type != "LAYERS")
			continue;
		layerIndexByName[b.name] = numLayers;
		numLayersByName[b.name] = b.subBlocks.size();
		CompileLayers(b, layersBuf);
		numLayers += b.subBlocks.size();
	}

	// WARNING: This assumes that all the floors are in the proper order
	bytebuf.WriteUInt32(floorsBlock->subBlocks.size());
	bytebuf.WriteUInt32(numLayers);
	bytebuf.WriteUInt32(templates.size());
	for (const LVLGENBADBlock &floorConf : floorsBlock->subBlocks)
	{
		std::string layersName = floorConf.properties.at("Layers").substr(1);

		int wallHeight = std::stoi(floorConf.properties.at("WallHeight"));
		int ambientLight = std::stoi(floorConf.properties.at("AmbientLight"));
		int fogDepth = std::stoi(floorConf.properties.at("FogDepth"));
		int tileset = std::stoi(floorConf.properties.at("Tileset"));
		int numLayers = numLayersByName[layersName];

		int enemiesId = -1;
		if (floorConf.properties.find("Enemies") != floorConf.properties.end())
			enemiesId = CompileEnemiesList(floorConf.properties.at("Enemies"));

		int flags = 0;
		int hasGrass = 0, hasLadderDown = 1, hasLadderUp = 1, hasEntrance = 1;
		if (floorConf.properties.find("HasGrass") != floorConf.properties.end())
			hasGrass = std::stoi(floorConf.properties.at("HasGrass"));
		if (floorConf.properties.find("HasLadderDown") != floorConf.properties.end())
			hasLadderDown = std::stoi(floorConf.properties.at("HasLadderDown"));
		if (floorConf.properties.find("HasLadderUp") != floorConf.properties.end())
			hasLadderUp = std::stoi(floorConf.properties.at("HasLadderUp"));
		if (floorConf.properties.find("HasEntrance") != floorConf.properties.end())
			hasEntrance = std::stoi(floorConf.properties.at("HasEntrance"));

		if (hasGrass)
			flags |= 1;
		if (hasLadderUp)
			flags |= (1 << 1);
		if (hasLadderDown)
			flags |= (1 << 2);
		if (!hasEntrance)
			flags |= (1 << 3);

		std::string bossName;
		int bossType = -1, bossColour = 0;
		if (floorConf.properties.find("BossName") != floorConf.properties.end() &&
			floorConf.properties.find("BossType") != floorConf.properties.end() &&
			floorConf.properties.find("BossColour") != floorConf.properties.end())
		{
			bossName = floorConf.properties.at("BossName");
			bossColour = std::stoi(floorConf.properties.at("BossColour"), nullptr, 16);
			bossColour |= 0xff000000; // Make sure the alpha value is set to 255
			std::string bossTypeName = floorConf.properties.at("BossType").substr(1);
			bossType = gReferenceIds[bossTypeName];
		}

		std::string levelName;
		if (floorConf.properties.find("EnterName") != floorConf.properties.end())
			levelName = floorConf.properties.at("EnterName");

		std::string songName;
		if (floorConf.properties.find("SongName") != floorConf.properties.end())
			songName = floorConf.properties.at("SongName");

		std::string skyName;
		if (floorConf.properties.find("SkyName") != floorConf.properties.end())
			skyName = floorConf.properties.at("SkyName");

		int fogColour = 0xff000000;
		if (floorConf.properties.find("FogColour") != floorConf.properties.end())
		{
			fogColour = std::stoi(floorConf.properties.at("FogColour"), nullptr, 16);
		}

		float particleSpeed = 0.0f;
		int particleColour = 0;
		if (floorConf.properties.find("AmbientParticleColour") != floorConf.properties.end() &&
			floorConf.properties.find("AmbientParticleSpeed") != floorConf.properties.end())
		{
			particleColour = std::stoi(floorConf.properties.at("AmbientParticleColour"), nullptr, 16);
			particleColour |= 0xff000000; // Make sure the alpha value is set to 255
			particleSpeed = std::stof(floorConf.properties.at("AmbientParticleSpeed"));
		}

		// Write level name
		if (levelName.empty())
		{
			bytebuf.FillZero(64);
		}
		else
		{
			bytebuf.WriteFixedString(levelName, 63);
			bytebuf.WriteUInt8(0);
		}
		// Write song name
		if (songName.empty())
		{
			PrintErrorMsg(floorConf.filepos, ERR_LVL_ERROR, "Missing SongName property for level");
		}
		else
		{
			bytebuf.WriteFixedString(songName, 31);
			bytebuf.WriteUInt8(0);
		}

		// Write boss name
		if (bossName.empty())
		{
			bytebuf.FillZero(32);
		}
		else
		{
			bytebuf.WriteFixedString(bossName, 31);
			bytebuf.WriteUInt8(0);
		}
		// Write sky name
		if (skyName.empty())
		{
			bytebuf.FillZero(20);
		}
		else
		{
			bytebuf.WriteFixedString(skyName, 19);
			bytebuf.WriteUInt8(0);
		}

		bytebuf.WriteInt8((char)wallHeight);
		bytebuf.WriteUInt8(ambientLight);
		bytebuf.WriteUInt8(fogDepth);
		bytebuf.WriteUInt8(tileset);
		bytebuf.WriteUInt32(flags);
		bytebuf.WriteUInt32(enemiesId);
		bytebuf.WriteUInt32(numLayers);
		bytebuf.WriteUInt32(layerIndexByName[layersName]);
		bytebuf.WriteUInt32(bossType);
		bytebuf.WriteUInt32(bossColour);
		bytebuf.WriteUInt32(fogColour);
		bytebuf.WriteUInt32(particleColour);
		bytebuf.WriteFloat(particleSpeed);
	}

	if (bytebuf.size() != headerSize)
	{
		std::cout << "Header too large!\n";
	}

	bytebuf.CopyFrom(layersBuf);

	// Place all the templates in a vector that is ordered by id instead of alphabetically.
	std::vector<Template> templatesVec;
	templatesVec.resize(templates.size());
	for (const auto &t : templates)
	{
		templatesVec[t.second.id] = t.second;
	}

	for (const auto &t : templatesVec)
	{
		bytebuf.WriteUInt16(t.width);
		bytebuf.WriteUInt16(t.height);
		bytebuf.WriteBytes((void *)t.tiles.c_str(), t.tiles.length());
		bytebuf.WriteUInt32(t.entities.size());
		for (const auto &e : t.entities)
		{
			int entityType = gReferenceIds[e.entityName];
			bytebuf.WriteUInt32(entityType);
			bytebuf.WriteFloat(e.x);
			bytebuf.WriteFloat(e.y);
		}
	}

	bytebuf.WriteUInt32(enemiesListCounter);
	bytebuf.CopyFrom(enemiesListBuf);

	bytebuf.SaveToFile(buildDir / "LVLGEN.bin");
}