#include <iostream>
#include <string>
#include <filesystem>
#include <unordered_map>

#ifdef _WIN32
// Who needs Windows.h?
extern "C" int IsDebuggerPresent();
#endif

#include "block.hpp"
#include "bytebuf.hpp"
#include "compiler.hpp"
#include "lvlgen.hpp"
#include "parser.hpp"
#include "structs.hpp"
#include "tokens.hpp"

namespace stdfs = std::filesystem;

// Names of the folders in the mod directory structure
#define DIR_TOOLS "bin"
#define DIR_OUTPUT "build"
#define DIR_SOURCE "defs"
#define DIR_GFX "gfx"
#define DIR_FONTS "fonts"
#define DIR_SFX "sfx"

struct {
	bool verbose;
} gFlags;

// The name of the current mod
std::string gModName;
// The path to the mod's root directory
stdfs::path gModPath;
// The path to the directory containing the other build tools
stdfs::path gToolsPath;

std::vector<Block> gBlocks;
std::vector<List> gStates;
std::unordered_map<std::string, ASTNode> gScripts;

// Map of IDs for states, entities and items, used by the compiler and serializers.
std::unordered_map<std::string, int> gReferenceIds;

bool ParseFile(const stdfs::path&);
bool CompileSource();
bool BuildResources();
bool BuildRPK();

// Uses `buildCmd` to convert the source file to the destination file, if needed.
// Returns -1 on error, 0 if the file already existed and 1 if the file was built.
int BuildIfNeeded(const std::string &buildCmd, const stdfs::path &src, const stdfs::path &dest, const std::string &args = "");

int main(int argc, char *argv[])
{
	int exitCode = 0;

	if (argc < 2) {
		std::cerr << "No mod directory specified" << std::endl;
		return EXIT_FAILURE;
	}

	gFlags.verbose = true;

	gModName = argv[1];
	gModPath = stdfs::current_path() / gModName;
	gToolsPath = stdfs::current_path() / DIR_TOOLS;

	// Parse all .rov files in the source code folder
	stdfs::path sourcePath = gModPath / DIR_SOURCE;
	try {
		for (const auto &direntry : stdfs::directory_iterator{ sourcePath }) {
			if (!direntry.is_regular_file() || direntry.path().extension() != ".rov")
				continue;
			if (!ParseFile(direntry.path())) {
				exitCode = EXIT_FAILURE;
				goto die;
			}
		}
	} catch (const std::exception &e) {
		std::cerr << "Unable to iterate definition directory: " << e.what() << std::endl;
		std::cerr << "Make sure to place your .rov files in " << sourcePath << std::endl;
		exitCode = EXIT_FAILURE;
		goto die;
	}

	if (!CompileSource()) {
		exitCode = EXIT_FAILURE;
		goto die;
	}
	if (!BuildResources()) {
		exitCode = EXIT_FAILURE;
		goto die;
	}
	if (!BuildRPK()) {
		exitCode = EXIT_FAILURE;
		goto die;
	}
	
die:
#ifdef _WIN32
	if (!IsDebuggerPresent())
		system("pause");
#endif

	return exitCode;
}

std::string PathAsStringWithQuotesIfThePathHasSpacesInIt(std::filesystem::path path)
{
	std::string result = path.string();
	if (result.find(' ') != std::string::npos)
		result = '"' + result + '"';
	return result;
}

bool ParseFile(const stdfs::path &path)
{
	std::string pathString = path.string();
	TokenReader reader(pathString.c_str());
	
	while (!reader.EndOfFile()) {
		token blockType = reader.ReadToken(TOK_NAME);
		token blockName = reader.ReadToken(TOK_NAME);
		if (blockType.contents == "SCRIPT") {
			gScripts[blockName.contents] = ParseScript(reader);
		} else if (blockType.contents == "STATE") {
			List stateList;
			stateList.name = blockName.contents;
			stateList.type = blockType.contents;
			stateList.filepos = reader.GetFilePos();

			token nextTok = reader.ReadToken(TOK_LIST_BEGIN);
			while ((nextTok = reader.ReadToken()).type != TOK_LIST_END)
				stateList.items.push_back(nextTok);

			gStates.push_back(stateList);
		} else {
			Block block;
			block.name = blockName.contents;
			block.type = blockType.contents;
			block.filepos = blockName.filepos;
			
			reader.ReadToken(TOK_BLOCK_BEGIN);
			while (reader.PeekToken().type != TOK_BLOCK_END) {
				token varName = reader.ReadToken(TOK_NAME);
				reader.ReadToken(TOK_ASSIGN);
				token varValue = reader.ReadToken();

				block.properties[varName.contents] = varValue;
			}
			reader.ReadToken(TOK_BLOCK_END);

			gBlocks.push_back(block);
		}
	}

	return true;
}

bool CompileSource()
{
	stdfs::path buildDir = gModPath / DIR_OUTPUT;
	stdfs::create_directories(buildDir);

	int stateCounter = 0;
	int entityCounter = 0;
	int itemCounter = 0;

	// Assign ids to blocks for entities and things and stuff
	for (const auto &block : gBlocks)
	{
		if (block.type == "ENTITY")
		{
			if (gReferenceIds.find(block.name) != gReferenceIds.end())
			{
				PrintErrorMsg(block.filepos, ERR_LVL_ERROR, "Duplicate name '%s'", block.name.c_str());
			}
			gReferenceIds[block.name] = entityCounter;
			entityCounter++;
		}
		else if (block.type == "ITEM")
		{
			if (gReferenceIds.find(block.name) != gReferenceIds.end())
			{
				PrintErrorMsg(block.filepos, ERR_LVL_ERROR, "Duplicate name '%s'", block.name.c_str());
			}
			gReferenceIds[block.name] = itemCounter;
			itemCounter++;
		}
	}

	for (const auto &state : gStates)
	{
		if (gReferenceIds.find(state.name) != gReferenceIds.end())
		{
			PrintErrorMsg(state.filepos, ERR_LVL_ERROR, "Duplicate name '%s'", state.name.c_str());
		}
		gReferenceIds[state.name] = stateCounter;
		stateCounter++;
	}

	printf("Building data...:\n");

	stdfs::path lvlgenFile = gModPath / "levelgen" / "floors.rov";
	auto stuff = ParseFloorgenFile(lvlgenFile.string());
	CompileFloorgen(stuff, buildDir);
	printf(" ...levelgen data\n");

	// Bytecode needs to go before states and entities, since action numbers refer to an offset within the whole bytecode block.
	ByteBuffer scriptData;
	scriptData.WriteUInt8(0); // Begin with 'end' instruction
	for (const auto &s : gScripts)
	{
		const ASTNode &node = s.second;
		//bytebuf.clear();

		//std::cout << s.first << ": \n";
		//DumpAST(node);
		//std::cout << "\n\n";
		
		gReferenceIds[s.first] = scriptData.size();
		//std::cout << s.first << " is at " << scriptData.size() << std::endl;
		CompileScript(node, scriptData);

		//std::filesystem::path path = "scripts";
		//path /= s.first;
		//bytebuf.SaveToFile(path);
	}
	WriteStringTable(scriptData);
	scriptData.SaveToFile(buildDir / "bytecode.bin");
	printf(" ...%ld scripts (%ld bytes)\n", gScripts.size(), scriptData.size());

	// Serialize other data structures
	ByteBuffer entityData, stateData, itemData;
	ByteBuffer entityNames, itemNames;
	itemData.WriteInt16(itemCounter);
	for (const auto &block : gBlocks)
	{
		if (block.type == "ENTITY")
		{
			SerializeEntity(entityData, block);
			entityNames.WriteLengthString(block.name);
			entityNames.WriteUInt16(gReferenceIds[block.name]);
		}
		else if (block.type == "ITEM")
		{
			SerializeItem(itemData, block);
			itemNames.WriteLengthString(block.name);
			itemNames.WriteUInt16(gReferenceIds[block.name]);
		}
	}
	printf(" ...%d entities, %d items\n", entityCounter, itemCounter);
	for (const auto &state : gStates)
	{
		SerializeState(stateData, state);
	}
	printf(" ...%d states\n", stateCounter);
	entityData.SaveToFile(buildDir / "entities.bin");
	stateData.SaveToFile(buildDir / "states.bin");
	itemData.SaveToFile(buildDir / "items.bin");
	entityNames.SaveToFile(buildDir / "ENTNAMES.bin");
	itemNames.SaveToFile(buildDir / "ITEMNAMES.bin");

	return true;
}

bool BuildResources()
{
	stdfs::path outputDir = gModPath / DIR_OUTPUT;

	// Convert sprites and graphics to the correct format using `convertimg`
	stdfs::path sourceDir = gModPath / DIR_GFX;
	try {
		for (const auto& direntry : std::filesystem::directory_iterator{ sourceDir }) {
			// Cut the thumbs off of Windows XP
			if (!direntry.is_regular_file() || direntry.path().extension() == ".db")
				continue;

			std::string command = PathAsStringWithQuotesIfThePathHasSpacesInIt(gToolsPath / "convertimg");
			auto outPath = outputDir / direntry.path().stem();

			if (BuildIfNeeded(command, direntry.path(), outPath) == -1) {
				std::cerr << "Unable to convert image " << direntry.path() << std::endl;
				return false;
			}
		}	
	} catch (const std::exception& e) {
		std::cerr << "Unable to iterate graphics directory: " << e.what() << std::endl;
		std::cerr << "Make sure to place your graphics in " << sourceDir << std::endl;
		return false;
	}

	// Build bitmap font files
	sourceDir = gModPath / DIR_FONTS;
	// TODO: Check errors
	std::string toolName = PathAsStringWithQuotesIfThePathHasSpacesInIt(gToolsPath / "bmfmake");
	BuildIfNeeded(toolName, sourceDir / "MiniSerif.png", outputDir / "MiniSerif.bmf");
	BuildIfNeeded(toolName, sourceDir / "Tiny.png", outputDir / "Tiny.bmf", "-fixed 3");

	return true;
}

bool BuildRPK()
{
	int rc;

	std::string cmdstr;
	cmdstr.append(PathAsStringWithQuotesIfThePathHasSpacesInIt(gToolsPath / "rpaker"));
	cmdstr.append(" -dir ");
	cmdstr.append(PathAsStringWithQuotesIfThePathHasSpacesInIt(gModPath / DIR_OUTPUT));
	cmdstr.append(" -dir ");
	cmdstr.append(PathAsStringWithQuotesIfThePathHasSpacesInIt(gModPath / DIR_SFX));
	cmdstr.append(" -out ");
	cmdstr.append(gModName + ".rpk");

	if (gFlags.verbose)
		std::cout << "> " << cmdstr << std::endl;
	
	if ((rc = system(cmdstr.c_str())) != 0) {
		std::cerr << "Could not build RPK file (exit code " << rc << ")" << std::endl;
		return false;
	}

	return true;
}

int BuildIfNeeded(const std::string &buildCmd, const stdfs::path &src, const stdfs::path &dest, const std::string &args)
{
	bool needsBuild = false;

	if (!stdfs::exists(src)) {
		std::cerr << "Unable to build missing file: " << src << std::endl;
		return -1;
	}

	if (!stdfs::exists(dest)) {
		needsBuild = true;
	} else {
		// Build if the source file was modified more recently than the output file
		needsBuild = stdfs::last_write_time(src) > stdfs::last_write_time(dest);
	}

	if (needsBuild) {
		std::string cmdstr;
		cmdstr.append(buildCmd);
		cmdstr.append(" ");
		cmdstr.append(args);
		cmdstr.append(" ");
		cmdstr.append(PathAsStringWithQuotesIfThePathHasSpacesInIt(src));
		cmdstr.append(" ");
		cmdstr.append(PathAsStringWithQuotesIfThePathHasSpacesInIt(dest));
		if (gFlags.verbose)
			std::cout << "> " << cmdstr << std::endl;
		if (system(cmdstr.c_str()) != 0)
			return -1;
	}

	return needsBuild;
}
