//--------------------------------------------------------------------------------------------------
// Conv2KT
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
//
// Congrats on looking at this code! I never had enough time to finish this, the info that came
// with the SDK should outline some basic usage of this tool, there is some initial prep work
// needed to get the original Conv2ZX folder functional here, specifically important is converting
// the 3DO CEL images to PNG, so the tool can grab metadata from them (and also to make the textures
// loadable in Killing Time: Resurrected). You can use this excellent tool by Trapexit to convert
// the CEL images to PNG: https://github.com/trapexit/3it/
// There are a number of unimplemented features and a few bugs with the tool, particularly is the
// West Wing having very broken UV coordinates for wall textures due to their use of a classic Doom
// mapping trick to have multiple wall textures on the same plane. Also missing is the ability to
// stretch ceiling and floor textures to fit their surface, generally the West Wing (ww.wad) is a good
// showcase for features yet to be added. There's also the problem of actors, and triggers, and the
// fact the 3DO version used level streaming to merge the final maps together on the fly.
// The enemies used in each level are a fun problem, the original game used an intricate makefile
// setup to create a special load order for assets in each level... We can't release those files, so
// in my opinion it'd probably be best to boot up the 3DO game to determine which enemies are
// present in each level... tbh it might even be easier to do that than dig through the old
// makefiles to figure it out.
// I don't think we implemented scale support for flat textures in KTR, which is a problem... Damn.
// The top of the file is comprised of code hastily ported from previous stuff I've worked on for
// the sake of prototyping, realistically that stuff should be split out into separate files.
// The code should build under Linux with a few minor tweaks if you want, Windows stuff is used to
// handle wildcard file stuff but it's optional.
// - Josh Dowell (Slartibarty) @ Nightdive
//
// NOT SUPPORTED:
//  UV flipped sidedefs (MIRROR)
//--------------------------------------------------------------------------------------------------

#include <cstdint>
#include <cstdlib>
#include <cmath>
#include <cstring>
#include <cstdio>
#include <cassert>

#include <algorithm>
#include <utility>
#include <string>
#include <vector>
#include <unordered_map>

// https://github.com/randy408/libspng
#include "../common/spng.h"

#include "doomdata.h"

#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <Windows.h>
#endif

// For Linux
#ifndef _Post_z_
#define _Post_z_
#endif

// The 3DO KT renderer does some things differently to Doom, this enables some processing that makes
// things look more like it should without engine changes
#define SIMULATE_3DO_KT

#define KT_MAX_PATH 1024

// sector flags
#define STOFIT		0x00000001		// Stretch to fit

//--------------------------------------------------------------------------------------------------
// Ported from my core library
//--------------------------------------------------------------------------------------------------

using int8		= int8_t;
using int16		= int16_t;
using int32		= int32_t;

using uint8		= uint8_t;
using uint16	= uint16_t;
using uint32	= uint32_t;

using strlen_t	= int32;

#define Q_strlen(a) static_cast<strlen_t>(strlen(a))
#define Q_stricmp _stricmp

void Q_strcpy_s(_Post_z_ char *pDest, strlen_t nDestSize, const char *pSrc)
{
	strncpy(pDest, pSrc, nDestSize - 1);
	pDest[nDestSize - 1] = '\0';
}

template< strlen_t nDestSize >
inline void Q_strcpy_s(_Post_z_ char(&pDest)[nDestSize], const char *pSrc)
{
	Q_strcpy_s(pDest, nDestSize, pSrc);
}

void Q_strcat_s(_Post_z_ char *pDest, strlen_t nDestSize, const char *pSrc)
{
	strncat(pDest, pSrc, nDestSize - Q_strlen(pDest) - 1);
	pDest[nDestSize - 1] = '\0';
}

template< strlen_t nDestSize >
inline void Q_strcat_s(_Post_z_ char(&pDest)[nDestSize], const char *pSrc)
{
	Q_strcat_s(pDest, nDestSize, pSrc);
}

inline bool IsPathSeparator(int c)
{
	return c == '/' || c == '\\';
}

const char *Str_GetFileExtension(const char *path)
{
	strlen_t len = Q_strlen(path);
	if (len <= 1)
	{
		return nullptr;
	}

	const char *src = path + len - 1;

	// Scan backwards until we find the full stop, or the start of the path
	while (src != path && *(src - 1) != '.')
	{
		--src;
	}

	// check to see if the '.' is part of a pathname
	if (src == path || IsPathSeparator(*src))
	{
		// Nope, no extension
		return nullptr;
	}

	return src;
}

// Tries to be fairly smart about stripping the extension off
void Str_StripExtension(const char *in, _Post_z_ char *out, strlen_t outSize)
{
	// Scan backwards for a full stop
	strlen_t end = Q_strlen(in) - 1;
	while (end > 0 && in[end] != '.' && IsPathSeparator(in[end]) == false)
	{
		--end;
	}

	if (end > 0 && IsPathSeparator(in[end]) == false && end < outSize)
	{
		// Found it
		strlen_t nChars = std::min(end, outSize - 1);
		if (out != in)
		{
			memcpy(out, in, nChars);
		}
		out[nChars] = '\0';
	}
	else
	{
		// Nope didn't find it
		if (out != in)
		{
			Q_strcpy_s(out, outSize, in);
		}
	}
}

void Str_SetExtension(const char *inPath, const char *extension, _Post_z_ char *outPath, strlen_t outPathLength)
{
	Str_StripExtension(inPath, outPath, outPathLength);
	Q_strcat_s(outPath, outPathLength, extension);
}

// Returns the last component of a path, which is probably a filename
const char *Str_GetFilename(const char *path)
{
	// Scan backwards until we hit the start, or we hit a path separator
	const char *cur = path + Q_strlen(path) - 1;
	while (cur > path && IsPathSeparator(*(cur - 1)) == false)
	{
		--cur;
	}
	return cur;
}

// Replaces every instance of 'a' with 'b' in the string
void Str_Substitute(char *pStr, int a, int b)
{
	for (; *pStr; ++pStr)
	{
		if (*pStr == a)
		{
			*pStr = (char)b;
		}
	}
}

//--------------------------------------------------------------------------------------------------
// Parser port, feel free to destroy and replace with something not hacked up
//--------------------------------------------------------------------------------------------------

//-------------------------------------------------------------------------------------------------
// Parse a token out of a string
// Silently truncates!!! Could easily be made to report overflows using an int pointer parameter
// that counts the number of chars skipped due to truncation
//
// Horrendously modified and mangled version of:
// https://github.com/id-Software/Quake-2/blob/master/game/q_shared.c#L1072
//-------------------------------------------------------------------------------------------------
static const char *COM_ParseEx(const char *data, char *token, int tokenlen)
{
	token[0] = 0;

	if (!data) {
		return nullptr;
	}

	//AssertMsgF(tokenlen >= 2, "Need at least a two byte buffer, size provided: %d", tokenlen);

	uint8 c;

skipwhite:
	// Skip whitespace
	for (; (c = *data) <= ' '; ++data)
	{
		if (c == 0) {
			return nullptr; // end of file
		}
	}

	// CONV2KT: Skip only hashtag comments!
	if (data[0] == '#')
	{
		// CONV2KT: Expect Mac format files!
		while (*data && *data != '\r') {
			++data;
		}
		goto skipwhite;
	}

	// Cursor
	strlen_t len = 0;

	// Parse a quoted string
	if (c == '\"')
	{
		++data;
		while (1)
		{
			c = *data++;
			if (c == '\"' || c == 0)
			{
				// End quote, Null terminate and return
				token[len] = 0;
				return data;
			}
			if (len < tokenlen - 1) {
				token[len++] = c;
			}
		}
	}

	// Parse a regular word
	do
	{
		if (len < tokenlen - 1) {
			token[len++] = c;
		}
		c = *++data;
	} while (c > ' ');

	// Null terminate and return
	token[len] = 0;
	return data;
}

//--------------------------------------------------------------------------------------------------
// Filesystem port
//--------------------------------------------------------------------------------------------------

using FileSize_t = size_t;

static FileSize_t FS_LoadFile(const char *filename, void **buffer, FileSize_t extraData = 0)
{
	//Assert(filename && filename[0]);
	//Assert(extraData >= 0);

	FILE *handle = fopen(filename, "rb");
	if (handle == nullptr)
	{
		if (buffer) {
			*buffer = nullptr;
		}
		return 0;
	}

	fseek(handle, 0, SEEK_END);
	FileSize_t length = ftell(handle);
	fseek(handle, 0, SEEK_SET);

	if (!buffer)
	{
		fclose(handle);
		return length;
	}

	if (length == 0)
	{
		// If it's 0kb we can't garner any information from this
		// if you need to test if a file exists, use FileExists
		fclose(handle);
		*buffer = nullptr;
		return 0;
	}

	void *buf = malloc(length + extraData);
	*buffer = buf;

	fread(buf, 1, length, handle);
	fclose(handle);

	memset((byte *)buf + length, 0, extraData);

	return length; // This used to return length + extraData, but that was mucking up VOX_ReadSentenceFile
}

//--------------------------------------------------------------------------------------------------
// Map structures
//--------------------------------------------------------------------------------------------------

struct wadinfo_t
{
	char		identification[4];		// should be IWAD
	int			numlumps;
	int			infotableofs;
};

struct filelump_t
{
	int			filepos;
	int			size;
	char		name[8];
};

// Extended sidedef
struct mapsidedef2_t : mapsidedef_t
{
	mapsidedef2_t() {
		memset(this, 0, sizeof(*this));
		scalex_top = 1.0;
		scalex_mid = 1.0;
		scalex_bottom = 1.0;
		scaley_top = 1.0;
		scaley_mid = 1.0;
		scaley_bottom = 1.0;
	}
	double scalex_top;
	double scalex_mid;
	double scalex_bottom;
	double scaley_top;
	double scaley_mid;
	double scaley_bottom;
};

static std::vector<mapthing_t>		g_things;
static std::vector<maplinedef_t>	g_linedefs;
static std::vector<mapsidedef2_t>	g_sidedefs;
static std::vector<mapvertex_t>		g_vertices;
static std::vector<mapsector_t>		g_sectors;

//--------------------------------------------------------------------------------------------------
// Map loader
//--------------------------------------------------------------------------------------------------

static maplump_t GetLumpType(const char *pName)
{
	if (strncmp(pName, "THINGS", 8) == 0)
	{
		return ML_THINGS;
	}
	if (strncmp(pName, "LINEDEFS", 8) == 0)
	{
		return ML_LINEDEFS;
	}
	if (strncmp(pName, "SIDEDEFS", 8) == 0)
	{
		return ML_SIDEDEFS;
	}
	if (strncmp(pName, "VERTEXES", 8) == 0)
	{
		return ML_VERTEXES;
	}
	if (strncmp(pName, "SECTORS", 8) == 0)
	{
		return ML_SECTORS;
	}
	return ML_INVALID;
}

static bool ValidLump(maplump_t type)
{
	switch (type)
	{
	case ML_THINGS:
	case ML_LINEDEFS:
	case ML_SIDEDEFS:
	case ML_VERTEXES:
	case ML_SECTORS:
		return true;
	default:
		return false;
	}
}

template<typename In, typename Out = In>
static void LoadLump(const filelump_t &lump, std::vector<Out> &elements, FILE *pFile)
{
	if (lump.size % sizeof(In))
	{
		// Funny lump size
		return;
	}

	const int numElements = lump.size / sizeof(In);

	elements.resize(numElements);

	for (int i = 0; i < numElements; ++i)
	{
		if (fread(&elements[i], sizeof(In), 1, pFile) != 1)
		{
			elements.clear();
			break;
		}
	}
}

static bool LoadMap(const char *pFilename)
{
	FILE *pFile = fopen(pFilename, "rb");
	if (!pFile)
	{
		return false;
	}

	wadinfo_t hdr;
	if (fread(&hdr, sizeof(hdr), 1, pFile) != 1)
	{
		fclose(pFile);
		return false;
	}

	if (strncmp(hdr.identification, "PWAD", 4) != 0)
	{
		fclose(pFile);
		return false;
	}

	if (fseek(pFile, hdr.infotableofs, SEEK_SET) != 0)
	{
		fclose(pFile);
		return false;
	}

	std::vector<filelump_t> lumps;
	lumps.resize(hdr.numlumps);
	if (fread(lumps.data(), sizeof(filelump_t), hdr.numlumps, pFile) != hdr.numlumps)
	{
		fclose(pFile);
		return false;
	}

	for (int i = 0; i < hdr.numlumps; ++i)
	{
		const filelump_t &lump = lumps[i];

		maplump_t type = GetLumpType(lump.name);
		if (!ValidLump(type))
		{
			continue;
		}

		if (fseek(pFile, lump.filepos, SEEK_SET) != 0)
		{
			continue;
		}

		switch (type)
		{
		case ML_THINGS:
			LoadLump<mapthing_t>(lump, g_things, pFile);
			break;
		case ML_LINEDEFS:
			LoadLump<maplinedef_t>(lump, g_linedefs, pFile);
			break;
		case ML_SIDEDEFS:
			LoadLump<mapsidedef_t, mapsidedef2_t>(lump, g_sidedefs, pFile);
			break;
		case ML_VERTEXES:
			LoadLump<mapvertex_t>(lump, g_vertices, pFile);
			break;
		case ML_SECTORS:
			LoadLump<mapsector_t>(lump, g_sectors, pFile);
			break;
		}
	}

	fclose(pFile);
	return true;
}

//--------------------------------------------------------------------------------------------------
// Script loader
//--------------------------------------------------------------------------------------------------

struct textureinfo_t
{
	int width, height;
};

static char g_artFolder[KT_MAX_PATH];
static std::unordered_map<std::string, std::string> g_artArray;
static std::unordered_map<std::string, textureinfo_t> g_textureinfo;

static void CleanAndCopyPath(char *pOutPath, const char *pInPath)
{
	if (pInPath[0] == ':')
	{
		strcpy(pOutPath, "./");
		++pInPath;
	}

	Q_strcat_s(pOutPath, KT_MAX_PATH, pInPath);
	Str_Substitute(pOutPath, ':', '/');
}

static bool LoadScript(const char *pFilename)
{
	char token[2048];

	void *pFile;
	FS_LoadFile(pFilename, &pFile, 1);
	if (!pFile)
	{
		return false;
	}

	const char *pBuffer = reinterpret_cast<const char *>(pFile);
	auto nextToken = [&]() -> bool { return (pBuffer = COM_ParseEx(pBuffer, token, sizeof(token))); };

	while (nextToken())
	{
startLoop:
		if (Q_stricmp(token, ">ARTHI") == 0)
		{
			nextToken();
			CleanAndCopyPath(g_artFolder, token);
			continue;
		}
		if (Q_stricmp(token, ">ARTLO") == 0)
		{
			nextToken();
			continue;
		}
		if (Q_stricmp(token, ">WAD") == 0)
		{
			nextToken();
			continue;
		}
		if (Q_stricmp(token, ">ARTARRAY") == 0)
		{
			while (nextToken())
			{
				if (token[0] == '>')
				{
					goto startLoop;
				}
				// lazy
				char key[2048];
				strcpy(key, token);
				nextToken();
				g_artArray.emplace(std::make_pair(std::string(key), std::string(token) + ".png"));
			}
			continue;
		}
		if (Q_stricmp(token, ">TRIGGER") == 0)
		{
			nextToken();
			nextToken();
			nextToken();
			continue;
		}
		if (Q_stricmp(token, ">YOFF") == 0)
		{
			nextToken();
			continue;
		}
		if (Q_stricmp(token, ">ZONE") == 0)
		{
			nextToken();
			nextToken();
			continue;
		}
		if (Q_stricmp(token, ">END") == 0)
		{
			break;
		}
	}

	free(pFile);
	return true;
}

static void FormatNewName(char *pOutName, strlen_t outLen, const char *pInName, bool local)
{
	const char *pUnq = Str_GetFilename(g_artFolder);
	Q_strcpy_s(pOutName, outLen, local ? "./hiRes/" : "textures/3do/");
	Q_strcat_s(pOutName, outLen, pUnq);
	Q_strcat_s(pOutName, outLen, pInName);
}

static void LoadTextureInfo(const char *pTextureName)
{
	char tempName[KT_MAX_PATH];

	if (pTextureName[0] == '\0' || pTextureName[0] == '-')
	{
		return;
	}

	auto search = g_artArray.find(pTextureName);
	if (search == g_artArray.end())
	{
		return;
	}

	FormatNewName(tempName, sizeof(tempName), search->second.c_str(), true);

	if (g_textureinfo.contains(tempName))
	{
		return;
	}

	FILE *handle = fopen(tempName, "rb");
	if (!handle)
	{
		return;
	}

	spng_ihdr ihdr;

	spng_ctx *ctx = spng_ctx_new(0);
	spng_set_png_file(ctx, handle);
	spng_get_ihdr(ctx, &ihdr);
	spng_ctx_free(ctx);

	fclose(handle);

	textureinfo_t &info = g_textureinfo[tempName];
	info.width = ihdr.width;
	info.height = ihdr.height;
}

static textureinfo_t GetTextureInfo(const char *pTextureName)
{
	char tempName[KT_MAX_PATH];

	if (pTextureName[0] == '\0' || pTextureName[0] == '-')
	{
		return textureinfo_t{};
	}

	strncpy(tempName, pTextureName, 8);
	tempName[8] = 0;

	auto search = g_artArray.find(tempName);
	if (search == g_artArray.end())
	{
		return textureinfo_t{};
	}

	FormatNewName(tempName, sizeof(tempName), search->second.c_str(), true);

	auto info = g_textureinfo.find(tempName);
	if (info != g_textureinfo.end())
	{
		return info->second;
	}

	return textureinfo_t{};
}

//--------------------------------------------------------------------------------------------------
// Fixups
//--------------------------------------------------------------------------------------------------

#ifdef SIMULATE_3DO_KT

struct sidestate_t
{
	const maplinedef_t *pLinedef = nullptr;

	mapsidedef2_t *pFrontSide = nullptr;
	mapsidedef2_t *pBackSide = nullptr;
	mapsector_t *pFrontSector = nullptr;
	mapsector_t *pBackSector = nullptr;
	int frontSectorHeight = -1;
	int backSectorHeight = -1;
};

struct mapvertex_hip_t
{
	int x, y;
};

static double VertexDistance(const mapvertex_t &v1, const mapvertex_t &v2)
{
	mapvertex_hip_t delta;
	delta.x = ((int)v1.x - (int)v2.x);
	delta.y = ((int)v1.y - (int)v2.y);
	return std::sqrt(delta.x*delta.x + delta.y*delta.y);
}

static void TweakSide(sidestate_t &state)
{
	if (!state.pFrontSide)
	{
		return;
	}

	const mapvertex_t &v1 = g_vertices[state.pLinedef->v1];
	const mapvertex_t &v2 = g_vertices[state.pLinedef->v2];
	const double dist = VertexDistance(v1, v2);

	// Scale the top texture
	if (state.pFrontSide->toptexture[0] && state.pFrontSide->toptexture[0] != '-')
	{
		textureinfo_t frontTopInfo = GetTextureInfo(state.pFrontSide->toptexture);
		if (frontTopInfo.width != 0 && frontTopInfo.height != 0)
		{
			// Adjust X
			if (frontTopInfo.width != dist)
			{
				state.pFrontSide->scalex_top = 1.0 / (dist / frontTopInfo.width);
			}

			// Do we have a back side?
			if (state.pBackSide)
			{
				int height1 = state.pBackSector->ceilingheight - state.pFrontSector->floorheight;
				int height2 = state.frontSectorHeight - height1;
				if (frontTopInfo.height != height2)
				{
					state.pFrontSide->scaley_top = 1.0 / ((double)height2 / frontTopInfo.height);
				}
			}
			else
			{
				// Ignore
			}
		}
	}

	// Scale the mid texture
	if (state.pFrontSide->midtexture[0] && state.pFrontSide->midtexture[0] != '-')
	{
		textureinfo_t frontMidInfo = GetTextureInfo(state.pFrontSide->midtexture);
		if (frontMidInfo.width != 0 && frontMidInfo.height != 0)
		{
			// Adjust X
			if (frontMidInfo.width != dist)
			{
				state.pFrontSide->scalex_mid = 1.0 / (dist / frontMidInfo.width);
			}

			// Do we have a back side?
			/*if (state.pBackSide)
			{
			}
			else*/
			{
				if (frontMidInfo.height != state.frontSectorHeight)
				{
					state.pFrontSide->scaley_mid = 1.0 / ((double)state.frontSectorHeight / frontMidInfo.height);
				}
			}
		}
	}

	// Scale the bottom texture
	if (state.pFrontSide->bottomtexture[0] && state.pFrontSide->bottomtexture[0] != '-')
	{
		textureinfo_t frontBtmInfo = GetTextureInfo(state.pFrontSide->bottomtexture);
		if (frontBtmInfo.width != 0 && frontBtmInfo.height != 0)
		{
			// Adjust X
			if (frontBtmInfo.width != dist)
			{
				state.pFrontSide->scalex_bottom = 1.0 / (dist / frontBtmInfo.width);
			}

			// Do we have a back side?
			if (state.pBackSide)
			{
				int height1 = state.pFrontSector->ceilingheight - state.pBackSector->floorheight;
				int height2 = state.frontSectorHeight - height1;
				if (frontBtmInfo.height != height2)
				{
					state.pFrontSide->scaley_bottom = 1.0 / ((double)height2 / frontBtmInfo.height);
				}
			}
			else
			{
				// Ignore
			}
		}
	}
}

static void TweakRelevantSidedefs(const maplinedef_t &linedef)
{
	const mapvertex_t &v1 = g_vertices[linedef.v1];
	const mapvertex_t &v2 = g_vertices[linedef.v2];

	mapsidedef2_t *pFrontSide = nullptr;
	mapsidedef2_t *pBackSide = nullptr;
	mapsector_t *pFrontSector = nullptr;
	mapsector_t *pBackSector = nullptr;
	int frontSectorHeight = -1;
	int backSectorHeight = -1;

	if (linedef.sidenum[0] != -1)
	{
		pFrontSide = &g_sidedefs[linedef.sidenum[0]];
		pFrontSector = &g_sectors[pFrontSide->sector];
		frontSectorHeight = pFrontSector->ceilingheight - pFrontSector->floorheight;
	}
	if (linedef.sidenum[1] != -1)
	{
		pBackSide = &g_sidedefs[linedef.sidenum[1]];
		pBackSector = &g_sectors[pBackSide->sector];
		backSectorHeight = pBackSector->ceilingheight - pBackSector->floorheight;
	}

	sidestate_t sidestate;
	sidestate.pLinedef = &linedef;
	sidestate.pFrontSide = pFrontSide;
	sidestate.pBackSide = pBackSide;
	sidestate.pFrontSector = pFrontSector;
	sidestate.pBackSector = pBackSector;
	sidestate.frontSectorHeight = frontSectorHeight;
	sidestate.backSectorHeight = backSectorHeight;

	// Front side
	TweakSide(sidestate);
	// Back side
	std::swap(sidestate.pFrontSide, sidestate.pBackSide);
	std::swap(sidestate.pFrontSector, sidestate.pBackSector);
	std::swap(sidestate.frontSectorHeight, sidestate.backSectorHeight);
	TweakSide(sidestate);
}

static void FixupFlats(mapsector_t &sector)
{

}

#endif

//--------------------------------------------------------------------------------------------------
// Thing translator
//--------------------------------------------------------------------------------------------------

#define	WEAPON_CATEGORY			0x00000000
#define	AMMO_CATEGORY			0x00000020
#define	POWER_CATEGORY			0x00000040
#define	CLOCK_CATEGORY			0x00000050
#define	KEY_CATEGORY			0x00000060
#define	DOOR_CATEGORY			0x00000070
#define	TESS_CATEGORY			0x00000080
#define	BUTTON_CATEGORY			0x000000A0
#define	TRIGGER_CATEGORY		0x000000B0
#define	UNUSED_1				0x00000010
#define	UNUSED_2				0x00000030
#define	FIELD_CATEGORY			0x000000C0
#define	TREE_CATEGORY			0x000000D0
#define	NME_CATEGORY			0x000000E0
#define	GHOST_CATEGORY			0x000000F0
#define	DECOY_CATEGORY			0x00000090

// use object category (type>>4) as index to proper value to add for proper sorting
static int categoryvec[] = {
	0x0000, // WEAPON_CATEGORY
	0x9000, // UNUSED_1
	0x1000, // AMMO_CATEGORY
	0xA000, // UNUSED_2
	0x2000, // POWER_CATEGORY
	0x3000, // CLOCK_CATEGORY
	0x4000, // KEY_CATEGORY
	0x5000, // DOOR_CATEGORY
	0x6000, // TESS_CATEGORY
	0x7000, // DECOY_CATEGORY
	0x8000, // BUTTON_CATEGORY
	0xB000, // TRIGGER_CATEGORY
	0xC000, // FIELD_CATEGORY
	0xD000, // TREE_CATEGORY
	0xE000, // NME_CATEGORY
	0xF000, // GHOST_CATEGORY
};

static void TranslateThing(mapthing_t &thing)
{
	int angle, att, type;

	angle = thing.angle;
	angle = ((angle/45)+6)&7; // n 0 - w 2 - s 4 - e 6

	att = thing.options;
	att = (att<<13)|((att&0x8)<<9); // shove mobile flag into bit 12, diff levs 13-15			

	// translate wad type to zx type
	type = thing.type;
	if (type >= 8000)
	{
		type -= 8000;  // force whatever value you want simply by adding 8000 to it
	}
	else
	{
		switch (type)
		{
		case 0xbbc:			// primary enemy
			type = 0xe1;
			break;
		case 0x9:			// secondary enemy
			type = 0xe2;
			break;
		case 0xbb9:
			type = 0xe4;	// tertiary enemy
			break;
		case 0xbba:
			type = 0xe7;	// enemy 1,2, or 3
			break;
		case 0x3a:
			type = 0xe3;	// enemy 1 or 2
			break;
		case 0xbbb:
			type = 0xe6;	// enemy 2 or 3
			break;
		case 0xbbe:
			type = 0xe5;	// enemy 1 or 3
			break;

		case 0x40:			// ghost
			type = 254;
			break;
		case 0x4c:			// Robert ghost
			type = 253;
			break;

		case 0x1:
			type = -1;
			//DEBUG(("Starting point (%d,%d). Ignored!\n", x>>16, z>>16));
			break;

		case 0x7d3:
			type = 0;
			break;
		case 0x7d1:
			type = 1;
			break;
		case 0x7d2:
			type = 2;
			break;
		case 0x7d4:
			type = 3;
			break;

		case 0x7d7:
			type = 32;
			break;
		case 0x7d8:
			type = 33;
			break;
		case 0x7da:
			type = 34;
			break;
		case 0x7ff:
			type = 35;
			break;

		case 0x800:
			type = 36;
			break;
		case 0x801:
			type = 37;
			break;
		case 0x7fe:
			type = 38;
			break;
		case 0x11:
			type = 39;
			break;

		case 0xd:
			type = 64;
			break;
		case 0x7de:
			type = 65;
			break;
		case 0x7db:
			type = 66;
			break;
		case 0x7dc:
			type = 67;
			break;
		case 0x7dd:
			type = 68;
			break;

		case 0x1e:
			type = 96;
			break;
		case 0x20:
			type = 97;
			break;
		case 0x2e:
			type = 98;
			break;
		case 0x37:
			type = 99;
			break;

		case 0xf:
			type = 128; // tess chest
			break;
		case 0x12:
			type = 129; // calves
			break;
		case 0x13:
			type = 130;	// arms
			break;
		case 0x14:
			type = 131;	// palms
			break;
		case 0x15:
			type = 132;	// feet
			break;
		case 0x16:
			type = 133; // head
			break;
		case 0xa:
			type = 134; // muscle
			break;
		case 0xc:
			type = 135; // ear
			break;
		case 0x1b:
			type = 136;	// mouth
			break;
		case 0x1c:
			type = 137;	// neck
			break;
		case 0x1d:
			type = 138;	// teeth
			break;

		case 0x22:
			type = 160;
			break;
		case 0x39:
			type = 161;
			break;
		case 0x7e8:
			type = 163;
			break;

		case 0x41:
			type = 144;	// decoy chest
			break;
		case 0x42:
			type = 145;	// calves 
			break;
		case 0x43:
			type = 146;	// arms
			break;
		case 0x44:
			type = 147;	// palms
			break;
		case 0x45:
			type = 148;	// feet
			break;
		case 0x46:
			type = 149;	// head
			break;
		case 0x47:
			type = 150;	// muscle
			break;
		case 0x48:
			type = 151;	// ear
			break;
		case 0x49:
			type = 152;	// mouth
			break;
		case 0x4a:
			type = 153;	// neck
			break;
		case 0x4b:
			type = 154;	// teeth
			break;

		case 0x4d:
			type = 204; // chair
			break;

		default:
			if ((type >= 0x100) && (type < 0x120)) // transmitters
			{
				att |= (thing.angle/45)<<2; // fold in rotation info
				angle = type - 0x100;
				printf("#CA - transmitter type in %d out %d\n", (int)type, (int)angle);
				type = 180;
			}
			else if ((type >= 0x120) && (type < 0x140)) // receivers
			{
				angle = type - 0x120;
				printf("#CA - receiver type in %d out %d\n", (int)type, (int)angle);
				type = 181;
			}
			else if ((type >= 0x140) && (type < 0x160)) // 2 way teleporters
			{
				att |= (thing.angle/45)<<2; // fold in rotation info
				angle = type - 0x140;
				printf("#CA - 2 way type in %d out %d\n", (int)type, (int)angle);
				type = 183;
			}
			else
			{
				if (type>=0x100)
				{
					printf("Error! Tried to output object type %d (0x%lx). Ignored it!!\n", type, type);
					type = -1;
				}
			}
		}
	}

}

//--------------------------------------------------------------------------------------------------
// UDMF writer
//--------------------------------------------------------------------------------------------------

using fixed_t = int32;
using angle_t = uint32;

#define FRACBITS	16
#define FRACUNIT	(1<<FRACBITS)

#define ANG45   0x20000000
#define ANG90   0x40000000
#define ANG180  0x80000000
#define ANG270  0xc0000000

//----------------------------------------------

template <typename T>
static T Max3(const T &a, const T &b, const T &c) { return std::max(a, std::max(b, c)); }

// Robust floating point comparisons:
// http://realtimecollisiondetection.net/blog/?p=89
static bool IsEqual(double f0, double f1, double epsilon) { return fabs(f0 - f1) <= epsilon * Max3(1.0, fabs(f0), fabs(f1)); }

static const char *BoolString(bool state) { return state ? "true" : "false"; }

//----------------------------------------------

static void StartBlock(FILE *pFile, const char *pName, int idx)							{ fprintf(pFile, "%s // %d\n{\n", pName, idx); }
static void EndBlock(FILE *pFile)														{ fprintf(pFile, "}\n\n"); }

static void WriteFlag(FILE *pFile, const char *pName, bool state)						{ if (state) fprintf(pFile, "%s = %s;\n", pName, BoolString(state)); }

static void WriteInt32(FILE *pFile, const char *pName, int32_t val)						{ fprintf(pFile, "%s = %d;\n", pName, val); }
static void WriteInt32_Opt(FILE *pFile, const char *pName, int32_t val, int32_t def)	{ if (val != def) WriteInt32(pFile, pName, val); }

static void WriteDouble(FILE *pFile, const char *pName, double val)						{ fprintf(pFile, "%s = %f;\n", pName, val); }
static void WriteDouble_Opt(FILE *pFile, const char *pName, double val, double def)		{ if (!IsEqual(val, def, 0.0001)) WriteDouble(pFile, pName, val); }

static void WriteString(FILE *pFile, const char *pName, const char *pVal)				{ fprintf(pFile, "%s = \"%s\";\n", pName, pVal); }

//----------------------------------------------

static void WriteThings(FILE *pFile)
{
	const int numThings = static_cast<int>(g_things.size());
	for (int i = 0; i < numThings; ++i)
	{
		mapthing_t &thing = g_things[i];
		//TranslateThing(thing); // TODO: Not yet

		StartBlock(pFile, "thing", i);

		WriteInt32(pFile, "x", thing.x);
		WriteInt32(pFile, "y", thing.y);
		WriteInt32(pFile, "angle", thing.angle / 45); // TODO: HELLO - Is this right??? ??? BIG UGLY ATTENTION GRABBING TEXT GO!!!
		WriteInt32(pFile, "type", thing.type);

		// TODO: FLAGS

		EndBlock(pFile);
	}
}

static void WriteLinedefs(FILE *pFile)
{
	const int numLinedefs = static_cast<int>(g_linedefs.size());
	for (int i = 0; i < numLinedefs; ++i)
	{
		const maplinedef_t &linedef = g_linedefs[i];
#ifdef SIMULATE_3DO_KT
		TweakRelevantSidedefs(linedef);
#endif

		// Start the block
		StartBlock(pFile, "linedef", i);

		WriteInt32(pFile, "v1", linedef.v1);
		WriteInt32(pFile, "v2", linedef.v2);
		WriteInt32_Opt(pFile, "sidefront", linedef.sidenum[0], -1);
		WriteInt32_Opt(pFile, "sideback", linedef.sidenum[1], -1);
		WriteInt32_Opt(pFile, "special", linedef.special, 0);
		WriteInt32_Opt(pFile, "id", linedef.tag, -1);
		WriteFlag(pFile, "blocking", linedef.flags & ML_BLOCKING);
		WriteFlag(pFile, "blockmonsters", linedef.flags & ML_BLOCKMONSTERS);
		WriteFlag(pFile, "twosided", linedef.flags & ML_TWOSIDED);
		WriteFlag(pFile, "dontpegtop", linedef.flags & ML_DONTPEGTOP);
		WriteFlag(pFile, "dontpegbottom", linedef.flags & ML_DONTPEGBOTTOM);
		WriteFlag(pFile, "secret", linedef.flags & ML_SECRET);
		WriteFlag(pFile, "blocksound", linedef.flags & ML_SOUNDBLOCK);
		WriteFlag(pFile, "dontdraw", linedef.flags & ML_DONTDRAW);
		WriteFlag(pFile, "mapped", linedef.flags & ML_MAPPED);

		// End the block
		EndBlock(pFile);
	}
}

static void WriteSidedefs(FILE *pFile)
{
	const int numSidedefs = static_cast<int>(g_sidedefs.size());
	for (int i = 0; i < numSidedefs; ++i)
	{
		const mapsidedef2_t &sidedef = g_sidedefs[i];

		// Start the block
		StartBlock(pFile, "sidedef", i);

		WriteInt32(pFile, "sector", sidedef.sector);
		WriteDouble_Opt(pFile, "offsetx_top", sidedef.textureoffset, 0);
		WriteDouble_Opt(pFile, "offsetx_mid", sidedef.textureoffset, 0);
		WriteDouble_Opt(pFile, "offsetx_bottom", sidedef.textureoffset, 0);
		WriteDouble_Opt(pFile, "offsety_top", sidedef.rowoffset, 0);
		WriteDouble_Opt(pFile, "offsety_mid", sidedef.rowoffset, 0);
		WriteDouble_Opt(pFile, "offsety_bottom", sidedef.rowoffset, 0);
		WriteDouble_Opt(pFile, "scalex_top", sidedef.scalex_top, 1);
		WriteDouble_Opt(pFile, "scalex_mid", sidedef.scalex_mid, 1);
		WriteDouble_Opt(pFile, "scalex_bottom", sidedef.scalex_bottom, 1);
		WriteDouble_Opt(pFile, "scaley_top", sidedef.scaley_top, 1);
		WriteDouble_Opt(pFile, "scaley_mid", sidedef.scaley_mid, 1);
		WriteDouble_Opt(pFile, "scaley_bottom", sidedef.scaley_bottom, 1);

		char tempName[256];

		char toptexture[9];
		char bottomtexture[9];
		char midtexture[9];

		strncpy(toptexture, sidedef.toptexture, 8);
		toptexture[8] = 0;
		strncpy(bottomtexture, sidedef.bottomtexture, 8);
		bottomtexture[8] = 0;
		strncpy(midtexture, sidedef.midtexture, 8);
		midtexture[8] = 0;

		if (toptexture[0] && toptexture[0] != '-')
		{
			auto search = g_artArray.find(toptexture);
			if (search != g_artArray.end())
			{
				FormatNewName(tempName, sizeof(tempName), search->second.c_str(), false);
				WriteString(pFile, "texturetop", tempName);
			}
			else
			{
				WriteString(pFile, "texturetop", toptexture);
			}
		}
		if (bottomtexture[0] && bottomtexture[0] != '-')
		{
			auto search = g_artArray.find(bottomtexture);
			if (search != g_artArray.end())
			{
				FormatNewName(tempName, sizeof(tempName), search->second.c_str(), false);
				WriteString(pFile, "texturebottom", tempName);
			}
			else
			{
				WriteString(pFile, "texturebottom", bottomtexture);
			}
		}
		if (midtexture[0] && midtexture[0] != '-')
		{
			auto search = g_artArray.find(midtexture);
			if (search != g_artArray.end())
			{
				FormatNewName(tempName, sizeof(tempName), search->second.c_str(), false);
				WriteString(pFile, "texturemiddle", tempName);
			}
			else
			{
				WriteString(pFile, "texturemiddle", midtexture);
			}
		}

		// End the block
		EndBlock(pFile);
	}
}

static void WriteVertices(FILE *pFile)
{
	const int numVerts = static_cast<int>(g_vertices.size());
	for (int i = 0; i < numVerts; ++i)
	{
		const mapvertex_t &vert = g_vertices[i];

		// Start the block
		StartBlock(pFile, "vertex", i);

		WriteInt32(pFile, "x", vert.x);
		WriteInt32(pFile, "y", vert.y);

		// End the block
		EndBlock(pFile);
	}
}

static void WriteSectors(FILE *pFile)
{
	const int numSector = static_cast<int>(g_sectors.size());
	for (int i = 0; i < numSector; ++i)
	{
		const mapsector_t &sector = g_sectors[i];

		// Start the block
		StartBlock(pFile, "sector", i);

		WriteInt32_Opt(pFile, "heightfloor", sector.floorheight, 0);
		WriteInt32_Opt(pFile, "heightceiling", sector.ceilingheight, 0);

		char tempName[256];

		char floorpic[9];
		char ceilingpic[9];

		strncpy(floorpic, sector.floorpic, 8);
		floorpic[8] = 0;
		strncpy(ceilingpic, sector.ceilingpic, 8);
		ceilingpic[8] = 0;

		if (floorpic[0] && floorpic[0] != '-')
		{
			auto search = g_artArray.find(floorpic);
			if (search != g_artArray.end())
			{
				FormatNewName(tempName, sizeof(tempName), search->second.c_str(), false);
				WriteString(pFile, "texturefloor", tempName);
			}
			else
			{
				WriteString(pFile, "texturefloor", floorpic);
			}
		}
		if (ceilingpic[0] && ceilingpic[0] != '-')
		{
			auto search = g_artArray.find(ceilingpic);
			if (search != g_artArray.end())
			{
				FormatNewName(tempName, sizeof(tempName), search->second.c_str(), false);
				WriteString(pFile, "textureceiling", tempName);
			}
			else
			{
				WriteString(pFile, "textureceiling", ceilingpic);
			}
		}

		WriteInt32_Opt(pFile, "lightlevel", sector.lightlevel, 160);
		WriteInt32_Opt(pFile, "special", sector.special, 0);
		WriteInt32_Opt(pFile, "tag", sector.tag, 0);

		// End the block
		EndBlock(pFile);
	}
}

static bool WriteUDMF(const char *pFilename)
{
	FILE *pFile = fopen(pFilename, "wb");
	if (!pFile)
	{
		return false;
	}

	// Seek past the header area
	fseek(pFile, sizeof(wadinfo_t), SEEK_SET);

	int start = ftell(pFile);
	fprintf(pFile, "namespace = \"ktex\";\n");
	WriteThings(pFile);
	WriteVertices(pFile);
	WriteLinedefs(pFile);
	WriteSidedefs(pFile);
	WriteSectors(pFile);
	int end = ftell(pFile);
	int total = end - start;

	// Write the directory lumps
	filelump_t lumpStartMap = {};
	filelump_t lumpTextMap = {};
	filelump_t lumpEndMap = {};

	// TODO: Just grab it from the input wad!
	strncpy(lumpStartMap.name, "MAP01", 8); // E2M1

	lumpTextMap.filepos = start;
	lumpTextMap.size = total;
	strncpy(lumpTextMap.name, "TEXTMAP", 8);

	strncpy(lumpEndMap.name, "ENDMAP", 8);

	int startDir = ftell(pFile);
	fwrite(&lumpStartMap, sizeof(lumpStartMap), 1, pFile);
	fwrite(&lumpTextMap, sizeof(lumpTextMap), 1, pFile);
	fwrite(&lumpEndMap, sizeof(lumpEndMap), 1, pFile);

	wadinfo_t hdr = {};
	strncpy(hdr.identification, "PWAD", 4);
	hdr.numlumps = 3;
	hdr.infotableofs = startDir;

	fseek(pFile, 0, SEEK_SET);
	fwrite(&hdr, sizeof(hdr), 1, pFile);

	// Now go away!!!
	fclose(pFile);

	return true;
}

//--------------------------------------------------------------------------------------------------

static bool OperateOnWad(const char *pSrc, const char *pDst)
{
	char inputName[KT_MAX_PATH];
	char outputName[KT_MAX_PATH];
	char scriptName[KT_MAX_PATH];

	Str_SetExtension(pSrc, ".wad", inputName, sizeof(inputName));
	if (pDst) {
		Str_SetExtension(pDst, ".wad", outputName, sizeof(outputName));
	} else {
		Str_StripExtension(inputName, outputName, sizeof(outputName));
		Q_strcat_s(outputName, "_udmf.wad");
	}
	Str_SetExtension(inputName, ".scrpt", scriptName, sizeof(scriptName));

	if (!LoadMap(inputName))
	{
		return false;
	}
	if (!LoadScript(scriptName))
	{
		return false;
	}

	printf("Converting \"%s\" to \"%s\"\n", inputName, outputName);

	char safeName[9];

	for (const mapsector_t &sector : g_sectors)
	{
		strncpy(safeName, sector.floorpic, 9);
		safeName[8] = 0;
		LoadTextureInfo(safeName);
		strncpy(safeName, sector.ceilingpic, 9);
		safeName[8] = 0;
		LoadTextureInfo(safeName);
	}

	for (const mapsidedef2_t &sidedef : g_sidedefs)
	{
		strncpy(safeName, sidedef.toptexture, 9);
		safeName[8] = 0;
		LoadTextureInfo(safeName);
		strncpy(safeName, sidedef.bottomtexture, 9);
		safeName[8] = 0;
		LoadTextureInfo(safeName);
		strncpy(safeName, sidedef.midtexture, 9);
		safeName[8] = 0;
		LoadTextureInfo(safeName);
	}

	if (!WriteUDMF(outputName))
	{
		return false;
	}

	// Clear out for the next run, in case it happens
	g_textureinfo.clear();
	g_artArray.clear();
	memset(g_artFolder, 0, sizeof(g_artFolder));

	g_sectors.clear();
	g_vertices.clear();
	g_sidedefs.clear();
	g_linedefs.clear();
	g_things.clear();

	return true;
}

int main(int argc, char **argv)
{
	if (argc < 2 || argc > 3)
	{
		printf("Not enough, or too many arguments!\n");
		return EXIT_FAILURE;
	}

#ifdef _WIN32
	char windowsIn[MAX_PATH];
	Q_strcpy_s(windowsIn, argv[1]);

	DWORD attr = GetFileAttributesA(windowsIn);
	if (attr == FILE_ATTRIBUTE_DIRECTORY)
	{
		Q_strcat_s(windowsIn, "*"); // hack

		WIN32_FIND_DATAA findData;
		HANDLE findHandle = FindFirstFileA(windowsIn, &findData);
		if (findHandle != INVALID_HANDLE_VALUE)
		{
			do {
				const char *pExt = Str_GetFileExtension(findData.cFileName);
				if (pExt && (Q_stricmp(pExt, "wad") == 0))
				{
					OperateOnWad(findData.cFileName, nullptr);
				}
			} while (FindNextFileA(findHandle, &findData));
		}
	}
	else
#endif
	{
		OperateOnWad(argv[1], (argc >= 3) ? argv[2] : nullptr);
	}

	return EXIT_SUCCESS;
}
