// Emacs style mode select   -*- C++ -*-
//-----------------------------------------------------------------------------
//
// $Id: 13d4b4db60411113beb7eeeec3d827e05e6900c8 $
//
// Copyright (C) 1993-1996 by id Software, Inc.
// Copyright (C) 2006-2026 by The Odamex Team.
//
// 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.
//
// DESCRIPTION:
//	Handles WAD file header, directory, lump I/O.
//
//-----------------------------------------------------------------------------


#include "odamex.h"

#ifdef UNIX
#include <ctype.h>
#ifndef O_BINARY
#define O_BINARY		0
#endif
#endif

#ifdef _WIN32
#include <io.h>
#else
#define strcmpi	strcasecmp
#endif

#include <fcntl.h>

#include "crc32.h"

#include "m_fileio.h"
#include "i_system.h"
#include "m_alloc.h"
#include "z_zone.h"
#include "cmdlib.h"
#include "m_argv.h"
#include "md5.h"

#include "farmhash.h"

#include "w_wad.h"

#include <sstream>
#include <algorithm>
#include <iomanip>


//
// GLOBALS
//

// Location of each lump on disk.
lumpinfo_t*		lumpinfo;
size_t			numlumps;

// Generation of handle.
// Takes up the first three bits of the handle id.  Starts at 1, increments
// every time we unload the current set of WAD files, and eventually wraps
// around from 7 to 1.  We skip 0 so a handle id of 0 can be considered NULL
// and part of no generation.
size_t handleGen = 1;
const size_t HANDLE_GEN_MASK = BIT_MASK(0, 2);
const size_t HANDLE_GEN_BITS = 3;

void**			lumpcache;

static unsigned	stdisk_lumpnum;

//
// W_LumpNameHash
//
// Hash function used for lump names. Must be mod'ed with table size.
// Can be used for any 8-character names.
// by Lee Killough
//
// [SL] taken from prboom-plus
//
unsigned int W_LumpNameHash(const char *s)
{
	unsigned int hash;

	(void)(( hash =        toupper(s[0]), s[1]) &&
			(hash = hash*3+toupper(s[1]), s[2]) &&
			(hash = hash*2+toupper(s[2]), s[3]) &&
			(hash = hash*2+toupper(s[3]), s[4]) &&
			(hash = hash*2+toupper(s[4]), s[5]) &&
			(hash = hash*2+toupper(s[5]), s[6]) &&
			(hash = hash*2+toupper(s[6]),
			(hash = hash*2+toupper(s[7])))
         );
	return hash;
}

//
// W_HashLumps
//
// killough 1/31/98: Initialize lump hash table
// [SL] taken from prboom-plus
//
void W_HashLumps(void)
{
	for (unsigned int i = 0; i < numlumps; i++)
		lumpinfo[i].index = -1;			// mark slots empty

	// Insert nodes to the beginning of each chain, in first-to-last
	// lump order, so that the last lump of a given name appears first
	// in any chain, observing pwad ordering rules. killough

	for (unsigned int i = 0; i < numlumps; i++)
	{
		unsigned int j = W_LumpNameHash(lumpinfo[i].name.c_str()) % (unsigned int)numlumps;
		lumpinfo[i].next = lumpinfo[j].index;     // Prepend to list
		lumpinfo[j].index = i;
	}
}


//
// uppercoppy
//
// [RH] Copy up to 8 chars, upper-casing them in the process
//
void uppercopy (char *to, const char *from)
{
	int i;

	for (i = 0; i < 8 && from[i]; i++)
		to[i] = toupper (from[i]);
	for (; i < 8; i++)
		to[i] = 0;
}

/**
 * @brief Calculate a CRC32 hash from a file.
 *
 * @param filename Filename of file to hash.
 * @return Output hash, or blank if file could not be found.
 */
OCRC32Sum W_CRC32(const std::string& filename)
{
	OCRC32Sum rvo;

	const int file_chunk_size = 8192;
	FILE* fp = fopen(filename.c_str(), "rb");

	if (!fp)
		return rvo;

	size_t n = 0;
	unsigned char buf[file_chunk_size];
	uint32_t crc = 0;

	while ((n = fread(buf, 1, sizeof(buf), fp)))
	{
		crc = crc32_fast(buf, n, crc);
	}

	std::string hashStr;

	hashStr = fmt::sprintf("%08X", crc);

	OCRC32Sum::makeFromHexStr(rvo, hashStr);
	return rvo; // bubble up failure
}

// denis - Standard MD5SUM
OMD5Hash W_MD5(const std::string& filename)
{
	OMD5Hash rvo;

	const int file_chunk_size = 8192;
	auto fp = uqFile(fopen(filename.c_str(), "rb"));

	if(!fp)
		return rvo;

	md5_state_t state;
	md5_init(&state);

	size_t n = 0;
	unsigned char buf[file_chunk_size];

	while((n = fread(buf, 1, sizeof(buf), fp.get())))
		md5_append(&state, (unsigned char *)buf, n);

	md5_byte_t digest[16];
	md5_finish(&state, digest);

	std::stringstream hashStr;

	for(int i = 0; i < 16; i++)
		hashStr << std::setw(2) << std::setfill('0') << std::hex << std::uppercase << (short)digest[i];

	OMD5Hash::makeFromHexStr(rvo, hashStr.str());
	return rvo; // bubble up failure
}

/*
 * @brief Creates a 128-bit fingerprint for a map via FarmHash.
 *
 * However, it encodes the fingerprint into a 16-byte array to be read later.
 *
 * @param lumpdata byte array pointer to the lump (or lumps) that needs to be fingerprinted.
 * @param size of the byte array pointer in bytes.
 * @return fhfprint_t - struct containing 16-byte array of fingerprint.
 */
fhfprint_t W_FarmHash128(const byte* lumpdata, int length)
{
	fhfprint_t fhfngprnt;

	if (!lumpdata)
		return fhfngprnt;

	util::uint128_t fingerprint128 = util::Fingerprint128((const char*)lumpdata, length);

	// Store the bytes of the hashes in the array.
	fhfngprnt.fingerprint[0] = fingerprint128.first >> 8 * 0;
	fhfngprnt.fingerprint[1] = fingerprint128.first >> 8 * 1;
	fhfngprnt.fingerprint[2] = fingerprint128.first >> 8 * 2;
	fhfngprnt.fingerprint[3] = fingerprint128.first >> 8 * 3;
	fhfngprnt.fingerprint[4] = fingerprint128.first >> 8 * 4;
	fhfngprnt.fingerprint[5] = fingerprint128.first >> 8 * 5;
	fhfngprnt.fingerprint[6] = fingerprint128.first >> 8 * 6;
	fhfngprnt.fingerprint[7] = fingerprint128.first >> 8 * 7;

	fhfngprnt.fingerprint[8] = fingerprint128.second >> 8 * 0;
	fhfngprnt.fingerprint[9] = fingerprint128.second >> 8 * 1;
	fhfngprnt.fingerprint[10] = fingerprint128.second >> 8 * 2;
	fhfngprnt.fingerprint[11] = fingerprint128.second >> 8 * 3;
	fhfngprnt.fingerprint[12] = fingerprint128.second >> 8 * 4;
	fhfngprnt.fingerprint[13] = fingerprint128.second >> 8 * 5;
	fhfngprnt.fingerprint[14] = fingerprint128.second >> 8 * 6;
	fhfngprnt.fingerprint[15] = fingerprint128.second >> 8 * 7;

	return fhfngprnt;
}

//
// LUMP BASED ROUTINES.
//

//
// W_AddLumps
//
// Adds lumps from the array of filelump_t. If clientonly is true,
// only certain lumps will be added.
//
void W_AddLumps(FILE* handle, const filelump_t* fileinfo, size_t newlumps, bool clientonly)
{
	lumpinfo = (lumpinfo_t*) M_Realloc(lumpinfo, (numlumps + newlumps) * sizeof(lumpinfo_t));
	if (!lumpinfo)
		I_Error("Couldn't realloc lumpinfo");

	lumpinfo_t* lump = &lumpinfo[numlumps];
	const filelump_t* info = &fileinfo[0];

	for (size_t i = 0; i < newlumps; i++, info++)
	{
		lump->handle = handle;
		lump->position = info->filepos;
		lump->size = info->size;
		lump->name = info->name;

		lump++;
		numlumps++;
	}
}


//
// W_AddFile
//
// All files are optional, but at least one file must be found
// (PWAD, if all required lumps are present).
// Files with a .wad extension are wadlink files with multiple lumps.
// Other files are single lumps with the base filename for the lump name.
//
// Map reloads are supported through WAD reload so no need for vanilla tilde
// reload hack here
//
void AddFile(const OResFile& file)
{
	uqFile handle;
	std::unique_ptr<filelump_t[]> fileinfo{};

	const std::string filename = file.getFullpath();

	if ( (handle = uqFile(fopen(filename.c_str(), "rb"))) == nullptr)
	{
		PrintFmt(PRINT_WARNING, "couldn't open {}\n", filename);
		return;
	}

	PrintFmt(PRINT_HIGH, "adding {}", filename);

	wadinfo_t header;
	size_t readlen = fread(&header, sizeof(header), 1, handle.get());
	if (readlen < 1)
	{
		PrintFmt(PRINT_HIGH, "failed to read {}.\n", filename);
		return;
	}
	header.identification = LELONG(header.identification);

	size_t newlumps;
	if (header.identification != IWAD_ID && header.identification != PWAD_ID)
	{
		// raw lump file
		std::string lumpname;
		M_ExtractFileBase(filename, lumpname);

		fileinfo = std::make_unique<filelump_t[]>(1);
		fileinfo[0].filepos = 0;
		fileinfo[0].size = M_FileLength(handle.get());
		std::transform(lumpname.c_str(), lumpname.c_str() + 8, fileinfo[0].name, toupper);

		newlumps = 1;
		PrintFmt(PRINT_HIGH, " (single lump)\n");
	}
	else
	{
		// WAD file
		header.numlumps = LELONG(header.numlumps);
		header.infotableofs = LELONG(header.infotableofs);
		size_t length = header.numlumps * sizeof(filelump_t);

		if (length > (unsigned)M_FileLength(handle.get()))
		{
			PrintFmt(PRINT_WARNING, "\nbad number of lumps for {}\n", filename);
			return;
		}

		fileinfo = std::make_unique<filelump_t[]>(header.numlumps);
		fseek(handle.get(), header.infotableofs, SEEK_SET);
		readlen = fread(fileinfo.get(), length, 1, handle.get());
		if (readlen < 1)
		{
			PrintFmt(PRINT_HIGH, "failed to read file info in {}\n", filename);
			return;
		}

		// convert from little-endian to target arch and capitalize lump name
		for (int i = 0; i < header.numlumps; i++)
		{
			fileinfo[i].filepos = LELONG(fileinfo[i].filepos);
			fileinfo[i].size = LELONG(fileinfo[i].size);
			std::transform(fileinfo[i].name, fileinfo[i].name + 8, fileinfo[i].name, toupper);
		}

		newlumps = header.numlumps;
		PrintFmt(PRINT_HIGH, " ({} lumps)\n", header.numlumps);
	}

	W_AddLumps(handle.release(), fileinfo.get(), newlumps, false);
}


//
//
// IsMarker
//
// (from BOOM)
//
//

static bool IsMarker (const lumpinfo_t *lump, const char *marker)
{
	return (lump->namespc == ns_global) && (!strncmp (lump->name.c_str(), marker, 8) ||
			(*(lump->name.c_str()) == *marker && !strncmp (lump->name.c_str() + 1, marker, 7)));
}

//
// W_MergeLumps
//
// Merge multiple tagged groups into one
// Basically from BOOM, too, although I tried to write it independently.
//

void W_MergeLumps (const OLumpName& start, const OLumpName& end, int space)
{
	// Some pwads use an icky hack to get flats with regular Doom.
	// This tries to detect them.
	size_t flatHack = 0;
	if (start == "F_START" && !Args.CheckParm("-noflathack"))
	{
		int fudge = 0;
		unsigned istart = 0;

		for (size_t i = 0; i < numlumps; i++) {
			if (IsMarker (lumpinfo + i, start.c_str()))
				fudge++, istart = i;
			else if (IsMarker (lumpinfo + i, end.c_str()))
				fudge--, flatHack = i;
		}
		if (istart > flatHack)
			fudge--;
		if (fudge >= 0)
			flatHack = 0;
	}

	auto newlumpinfos = std::make_unique<lumpinfo_t[]>(numlumps);

	size_t newlumps = 0;
	size_t oldlumps = 0;
	bool insideBlock = false;

	for (size_t i = 0; i < numlumps; i++)
	{
		if (!insideBlock)
		{
			// Check if this is the start of a block
			if (IsMarker (lumpinfo + i, start.c_str()))
			{
				insideBlock = true;

				// Create start marker if we haven't already
				if (!newlumps)
				{
					newlumps++;
					newlumpinfos[0].name = start;
					newlumpinfos[0].handle = NULL;
					newlumpinfos[0].position =
						newlumpinfos[0].size = 0;
					newlumpinfos[0].namespc = ns_global;
				}
			}
			else
			{
				// Copy lumpinfo down this list
				lumpinfo[oldlumps++] = lumpinfo[i];
			}
		}
		else
		{
			// Check if this is the end of a block
			if (flatHack)
			{
				if (flatHack == i)
				{
					insideBlock = false;
					flatHack = 0;
				}
				else
				{
					if (lumpinfo[i].size != 4096)
					{
						lumpinfo[oldlumps++] = lumpinfo[i];
					}
					else
					{
						newlumpinfos[newlumps] = lumpinfo[i];
						newlumpinfos[newlumps++].namespc = space;
					}
				}
			}
			else if (i && lumpinfo[i].handle != lumpinfo[i-1].handle)
			{
				// Blocks cannot span multiple files
				insideBlock = false;
				lumpinfo[oldlumps++] = lumpinfo[i];
			}
			else if (IsMarker (lumpinfo + i, end.c_str()))
			{
				// It is. We'll add the end marker once
				// we've processed everything.
				insideBlock = false;
			}
			else
			{
				newlumpinfos[newlumps] = lumpinfo[i];
				newlumpinfos[newlumps++].namespc = space;
			}
		}
	}

	// Now copy the merged lumps to the end of the old list
	// and create the end marker entry.

	if (newlumps)
	{
		if (oldlumps + newlumps > numlumps)
			lumpinfo = (lumpinfo_t*) M_Realloc(lumpinfo, oldlumps + newlumps);

		std::copy_n(newlumpinfos.get(), newlumps, lumpinfo + oldlumps);

		numlumps = oldlumps + newlumps;

		lumpinfo[numlumps].name = end;
		lumpinfo[numlumps].handle = NULL;
		lumpinfo[numlumps].position =
			lumpinfo[numlumps].size = 0;
		lumpinfo[numlumps].namespc = ns_global;
		numlumps++;
	}
}

//
// W_InitMultipleFiles
// Pass a null terminated list of files to use.
// All files are optional, but at least one file
//  must be found.
// Files with a .wad extension are idlink files
//  with multiple lumps.
// Other files are single lumps with the base filename
//  for the lump name.
// Lump names can appear multiple times.
// The name searcher looks backwards, so a later file
//  does override all earlier ones.
//
void W_InitMultipleFiles(const OResFiles& files)
{
	// [EB] Fix for use-after-free when OZone tries to null the "user" pointers
	if (lumpcache)
	{
		for (size_t i = 0; i < numlumps; i++)
			Z_Free(lumpcache[i]);
	}

	// open all the files, load headers, and count lumps
	// will be realloced as lumps are added
	::numlumps = 0;

	M_Free(::lumpinfo);
	::lumpinfo = NULL;

	// open each file once, load headers, and count lumps
	std::vector<OMD5Hash> loaded;
	for (size_t i = 0; i < files.size(); i++)
	{
		if (std::find(loaded.begin(), loaded.end(), files.at(i).getMD5()) ==
		    loaded.end())
		{
			AddFile(files.at(i));
			loaded.push_back(files.at(i).getMD5());
		}
	}

	if (!numlumps)
		I_Error("W_InitFiles: no files found");

	// [RH] Set namespace markers to global for everything
	for (size_t i = 0; i < numlumps; i++)
		lumpinfo[i].namespc = ns_global;

	// [RH] Merge sprite and flat groups.
	//		(We don't need to bother with patches, since
	//		Doom doesn't use markers to identify them.)
	W_MergeLumps ("TX_START", "TX_END", ns_textures);
	W_MergeLumps ("S_START", "S_END", ns_sprites); // denis - fixme - security
	W_MergeLumps ("F_START", "F_END", ns_flats);
	W_MergeLumps ("C_START", "C_END", ns_colormaps);

    // set up caching
	M_Free(lumpcache);

	size_t size = numlumps * sizeof(*lumpcache);
	lumpcache = (void **) M_Malloc(size);

	if (!lumpcache)
		I_Error("Couldn't allocate lumpcache");

	memset (lumpcache,0, size);

	// killough 1/31/98: initialize lump hash table
	W_HashLumps();

	stdisk_lumpnum = W_GetNumForName("STDISK");
}

/**
 * @brief Return a handle for a given lump.
 */
lumpHandle_t W_LumpToHandle(const unsigned lump)
{
	lumpHandle_t rvo;
	size_t id = static_cast<size_t>(lump) << HANDLE_GEN_BITS;
	rvo.id = id | ::handleGen;
	return rvo;
}

/**
 * @brief Return a lump for a given handle, or -1 if the handle is invalid.
 */
int W_HandleToLump(const lumpHandle_t handle)
{
	size_t gen = handle.id & HANDLE_GEN_MASK;
	if (gen != ::handleGen)
	{
		return -1;
	}
	const unsigned lump = handle.id >> HANDLE_GEN_BITS;
	if (lump >= ::numlumps)
	{
		return -1;
	}
	return lump;
}

//
// W_CheckNumForName
// Returns -1 if name not found.
//
// Rewritten by Lee Killough to use hash table for performance. Significantly
// cuts down on time -- increases Doom performance over 300%. This is the
// single most important optimization of the original Doom sources, because
// lump name lookup is used so often, and the original Doom used a sequential
// search. For large wads with > 1000 lumps this meant an average of over
// 500 were probed during every search. Now the average is under 2 probes per
// search. There is no significant benefit to packing the names into longwords
// with this new hashing algorithm, because the work to do the packing is
// just as much work as simply doing the string comparisons with the new
// algorithm, which minimizes the expected number of comparisons to under 2.
//
// [SL] taken from prboom-plus
//
int W_CheckNumForName(const char *name, int namespc)
{
	// Hash function maps the name to one of possibly numlump chains.
	// It has been tuned so that the average chain length never exceeds 2.

	// proff 2001/09/07 - check numlumps==0, this happens when called before WAD loaded
	int i = (numlumps == 0 || name == nullptr)?(-1):(lumpinfo[W_LumpNameHash(name) % numlumps].index);

	// We search along the chain until end, looking for case-insensitive
	// matches which also match a namespace tag. Separate hash tables are
	// not used for each namespace, because the performance benefit is not
	// worth the overhead, considering namespace collisions are rare in
	// Doom wads.

	while (i >= 0 && (strnicmp(lumpinfo[i].name.c_str(), name, 8) ||
				lumpinfo[i].namespc != namespc))
		i = lumpinfo[i].next;

	// Return the matching lump, or -1 if none found.
	return i;
}

//
// W_GetNumForName
// Calls W_CheckNumForName, but bombs out if not found.
//
int W_GetNumForName(const char* name, int namespc)
{
	int i = W_CheckNumForName(name, namespc);

	if (i == -1)
	{
		I_Error("W_GetNumForName: {} not found!\n(checked in: {})", name,
		        M_ResFilesToString(::wadfiles));
	}

	return i;
}

/**
 * @brief Return the name of a lump number.
 *
 * @detail You likely only need this for debugging, since a name can be
 *         ambiguous.
 */
OLumpName W_LumpName(unsigned lump)
{
	if (lump >= ::numlumps)
		I_Error("{}: {} >= numlumps", __FUNCTION__, lump);

	return ::lumpinfo[lump].name;
}

//
// W_LumpLength
// Returns the buffer size needed to load the given lump.
//
unsigned W_LumpLength (unsigned lump)
{
	if (lump >= numlumps)
		I_Error("W_LumpLength: {} >= numlumps", lump);

	return lumpinfo[lump].size;
}



//
// W_ReadLump
// Loads the lump into the given buffer,
//  which must be >= W_LumpLength().
//
void W_ReadLump(unsigned int lump, void* dest)
{
	int		c;
	lumpinfo_t*	l;

	if (lump >= numlumps)
		I_Error("W_ReadLump: {} >= numlumps", lump);

	l = lumpinfo + lump;

	if (lump != stdisk_lumpnum)
    	I_BeginRead();

	fseek (l->handle, l->position, SEEK_SET);
	c = fread (dest, l->size, 1, l->handle);

	if (feof(l->handle))
		I_Error("W_ReadLump: only read {} of {} on lump {}", c, l->size, lump);

	if (lump != stdisk_lumpnum)
    	I_EndRead();
}

//
// W_ReadChunk
//
// denis - for wad downloading
//
unsigned W_ReadChunk (const char *file, unsigned offs, unsigned len, void *dest, unsigned &filelen)
{
	auto fp = uqFile(fopen(file, "rb"));
	unsigned read = 0;

	if(fp)
	{
		filelen = M_FileLength(fp.get());

		fseek(fp.get(), offs, SEEK_SET);
		read = fread(dest, 1, len, fp.get());
	}
	else
		filelen = 0;

	return read;
}


//
// W_CheckLumpName
//
bool W_CheckLumpName (unsigned lump, const char *name)
{
	if (lump >= numlumps)
		return false;

	return !strnicmp (lumpinfo[lump].name.c_str(), name, 8);
}

//
// W_GetLumpName
//
void W_GetLumpName(char *to, unsigned lump)
{
	if (lump >= numlumps)
		*to = 0;
	else
	{
		memcpy (to, lumpinfo[lump].name.c_str(), 8); // denis - todo -string limit?
		to[8] = '\0';
		std::transform(to, to + strlen(to), to, toupper);
	}
}

//
// W_GetOLumpName
//
void W_GetOLumpName(OLumpName& to, unsigned lump)
{
	if (lump >= numlumps)
		to.clear();
	else
		to = lumpinfo[lump].name;
}

//
// W_GetOLumpName
//
OLumpName W_GetOLumpName(unsigned lump)
{
	return lumpinfo[lump].name;
}

//
// W_CacheLumpNum
//
void* W_CacheLumpNum(unsigned int lump, const zoneTag_e tag)
{
	if ((unsigned)lump >= numlumps)
		I_Error("W_CacheLumpNum: {} >= numlumps",lump);

	if (!lumpcache[lump])
	{
		// read the lump in
		// [RH] Allocate one byte more than necessary for the
		//		lump and set the extra byte to zero so that
		//		various text parsing routines can just call
		//		W_CacheLumpNum() and not choke.

		//DPrintf("cache miss on lump %i\n",lump);
		unsigned int lump_length = W_LumpLength(lump);
		lumpcache[lump] = (byte *)Z_Malloc(lump_length + 1, tag, &lumpcache[lump]);
		W_ReadLump(lump, lumpcache[lump]);
		*((unsigned char*)lumpcache[lump] + lump_length) = 0;
	}
	else
	{
		//printf ("cache hit on lump %i\n",lump);
		Z_ChangeTag(lumpcache[lump],tag);
	}

	return lumpcache[lump];
}

//
// W_CacheLumpName
//
void* W_CacheLumpName(const char* name, const zoneTag_e tag)
{
	return W_CacheLumpNum(W_GetNumForName(name), tag);
}

//
// W_CacheLumpName
//
void* W_CacheLumpName(const OLumpName& name, const zoneTag_e tag)
{
	return W_CacheLumpNum(W_GetNumForName(name), tag);
}

size_t R_CalculateNewPatchSize(patch_t *patch, size_t length);
void R_ConvertPatch(patch_t* rawpatch, patch_t* newpatch, const unsigned int lumpnum);

//
// W_CachePatch
//
// [SL] Reads and caches a patch from disk. This takes care of converting the
// patch from the standard Doom format of posts with 1-byte lengths and offsets
// to a new format for posts that uses 2-byte lengths and offsets.
//
patch_t* W_CachePatch(unsigned lumpnum, const zoneTag_e tag)
{
	if (lumpnum >= numlumps)
		I_Error("W_CachePatch: {} >= numlumps", lumpnum);

	if (!lumpcache[lumpnum])
	{
		// temporary storage of the raw patch in the old format
		auto rawlumpdata = std::make_unique<byte[]>(W_LumpLength(lumpnum));

		W_ReadLump(lumpnum, rawlumpdata.get());
		patch_t *rawpatch = (patch_t*)(rawlumpdata.get());

		size_t newlumplen = R_CalculateNewPatchSize(rawpatch, W_LumpLength(lumpnum));

		if (newlumplen > 0)
		{
			// valid patch
			lumpcache[lumpnum] = (byte *)Z_Malloc(newlumplen + 1, tag, &lumpcache[lumpnum]);
			patch_t *newpatch = (patch_t*)lumpcache[lumpnum];
			*((unsigned char*)lumpcache[lumpnum] + newlumplen) = 0;

			R_ConvertPatch(newpatch, rawpatch, lumpnum);
		}
		else
		{
			// invalid patch - just create a header with width = 0, height = 0
			lumpcache[lumpnum] = Z_Malloc(sizeof(patch_t), tag, &lumpcache[lumpnum]);
			memset(lumpcache[lumpnum], 0, sizeof(patch_t));
		}
	}
	else
	{
		Z_ChangeTag(lumpcache[lumpnum], tag);
	}

	// denis - todo - would be good to check whether the patch violates W_LumpLength here
	// denis - todo - would be good to check for width/height == 0 here, and maybe replace those with a valid patch

	return (patch_t*)lumpcache[lumpnum];
}

patch_t* W_CachePatch(const char* name, const zoneTag_e tag)
{
	return W_CachePatch(W_GetNumForName(name), tag);
	// denis - todo - would be good to replace non-existant patches with a default '404' patch
}

patch_t* W_CachePatch(const OLumpName& name, const zoneTag_e tag)
{
	return W_CachePatch(W_GetNumForName(name), tag);
	// denis - todo - would be good to replace non-existant patches with a default '404'
	// patch
}

/**
 * @brief Cache a patch by lump number and return a handle to it.
 */
lumpHandle_t W_CachePatchHandle(const int lumpNum, const zoneTag_e tag)
{
	W_CachePatch(lumpNum, tag);
	return W_LumpToHandle(lumpNum);
}

/**
 * @brief Cache a patch by name and namespace and return a handle to it.
 */
lumpHandle_t W_CachePatchHandle(const char* name, const zoneTag_e tag, const int ns)
{
	return W_CachePatchHandle(W_GetNumForName(name, ns), tag);
}

/**
 * @brief Cache a patch by name and namespace and return a handle to it.
 */
lumpHandle_t W_CachePatchHandle(const OLumpName& name, const zoneTag_e tag, const int ns)
{
	return W_CachePatchHandle(W_GetNumForName(name, ns), tag);
}

/**
 * @brief Resolve a handle into a patch_t, or an empty patch if the
 *        lump was missing or from a previous generation.
 */
patch_t* W_ResolvePatchHandle(const lumpHandle_t lump)
{
	int lumpnum = W_HandleToLump(lump);
	if (lumpnum == -1)
	{
		static patch_t empty;
		memset(&empty, 0, sizeof(patch_t));
		return &empty;
	}
	return static_cast<patch_t*>(lumpcache[lumpnum]);
}

//
// W_FindLump
//
// Find a named lump. Specifically allows duplicates for merging of e.g.
// SNDINFO lumps.
//
// [SL] 2013-01-15 - Search forwards through the list of lumps in reverse pwad
// ordering, returning older lumps with a matching name first.
// Initialize search with lastlump == -1 before calling for the first time.
//
int W_FindLump (const char *name, int lastlump)
{
	if (lastlump < -1)
		lastlump = -1;

	for (int i = lastlump + 1; i < (int)numlumps; i++)
	{
		if (strnicmp(lumpinfo[i].name.c_str(), name, 8) == 0)
			return i;
	}

	return -1;
}

//
// W_Close
//
// Close all open files
//

void W_Close ()
{
	// store closed handles, so that fclose isn't called multiple times
	// for the same handle
	std::vector<FILE *> handles;

	lumpinfo_t * lump_p = lumpinfo;
	while (lump_p < lumpinfo + numlumps)
	{
		// if file not previously closed, close it now
		if(lump_p->handle &&
		   std::find(handles.begin(), handles.end(), lump_p->handle) == handles.end())
		{
			fclose(lump_p->handle);
			handles.push_back(lump_p->handle);
		}
		lump_p++;
	}

	::handleGen = (::handleGen + 1) & HANDLE_GEN_MASK;
	if (::handleGen == 0)
	{
		// 0 is reserved for the NULL handle.
		::handleGen += 1;
	}
}

VERSION_CONTROL (w_wad_cpp, "$Id: 13d4b4db60411113beb7eeeec3d827e05e6900c8 $")
