/*************************************/
/*                                   */
/*   MakeUberMake.cpp                */
/*   tools  09/14/02                 */
/*   ned martin  avalanche software  */
/*   visual c++ 6.0                  */
/*                                   */
/*************************************/

/* this program parses an uberfile log file to create a makefile for a world uberfile. the intent is to automate the
process. the log file may contain a list of all files loaded for a particular world, and it may contain the lists for
multiple worlds

the makefile has some constant text at the top that changes very little between worlds, and then lists of the files to
go into the uberfile. the order of these files is important, which is why this program was written. also, if we
decide in the future that there is a better order, this program can be updated and re-run.

the main issue in uberfiles is time spent in _dblCollapse. since the uberfile is big, memcpy's take up measurable time,
so if we can organize the uberfile to minimize this we are helping with load times */

/******************* includes ****************************/

#include <windows.h>
#include <stdio.h>
#include <io.h>
#include <sys/stat.h>
#include <assert.h>

#include "resource.h"

/******************* defines *****************************/

#define MAX_FILES_IN_LIST	1000	// WAY overkill!
#define VERSION_ID			3

/* file types */

#define UNKNOWN_FILE			0
#define DBL_FILE				1
#define FILESTREAM_FILE		2

/* supported platforms */

#define PLATFORM_WIN32	0
#define PLATFORM_PS2		1
#define PLATFORM_GCN		2
#define MAX_PLATFORMS	3

/******************* macros ******************************/

/******************* structures **************************/

typedef struct
{
	char	Name[_MAX_PATH];
	int	nFileSize;
	int	nFlushedBytes;
	bool	bPlatformDependent;
	int	nType;
} ts_FileInfo;

typedef struct
{
	char	*InputExt;
	char	*OutputExt;
} ts_Translate;

/* for sn link map analysis code that's piggybacked onto MakeUberMaker */

typedef struct
{
	int	nAddress;
	int	nSegmentSize;
	int	nClassSize;
	int	nSize;
	char	*pSegment;
	char	*pClass;
	char	*pName;
} ts_Record;

/******************* externs *****************************/

/******************* global variables ********************/

/******************* local variables *********************/

ts_FileInfo Files[MAX_FILES_IN_LIST];
int nFileCount = 0;
char JujuPath[_MAX_PATH];
char DataFolder[_MAX_PATH];
bool bQuit = false;
HWND hDialog;
char DialogText[200];

char *MakefileHeader[] = {

	"#",
	"# This file was generated by MakeUberMake.exe",
	"#",
	"# Child makefile for making BigJuju uber files",
	"# This makefile is designed to work with OpusMake (make.exe).",
	"#",
	"#",
	"# see comments in makefile in BigJuju root. this makefile is part of a recursive chain of makefiles",
	"# that build uberfiles",
	"",
	"# includes",
	"",
	"include $(JUJU_IMAGE_DIR)\\Uber.mk",
	"",
	"# world this makefile creates",
	"",
	NULL};	// must be last

char *MakefilePaths[] = {

	"",
	"# paths to current work directories. our current directory should be under the Data_PC (or Data_PS2 or Data_GCN)",
	"# directory. but DBLMerge needs to have some path information to make dblFileNamesMatch work when searching for",
	"# already loaded files, so i can't just use \".\"",
	"",
	"PI_PATH = ..\\..\\..\\Data",
	"PD_PATH = ..\\..",
	"",
	"# search path-- look in platform-independent path first, then platform-dependent. these are needed for searching",
	"# for inferred source data-- data to use to make intermediate files necessary for DBLMerge, like making a .dcd",
	"# file out a .cdb file",
	"",
	".PATH = $(PI_PATH)\\levels\\$(WORLD);$(PD_PATH)\\levels\\$(WORLD);$(PI_PATH)\\Fonts;$(PI_PATH)\\Tables;$(PI_PATH)\\Var;$(PI_PATH)\\Players\\Dave;$(PD_PATH);$(PD_PATH)\\music;$(PD_PATH)\\music\\$(WORLD)",
	"",
	"# target(s). these may be directories to recurse into",
	"",
	"TARGET_FILE = ..\\..\\$(WORLD).dbu",
	"",
	"# rules",
	"",
	"all : $(TARGET_FILE)",
	NULL};	// must be last
	
char *MakefileFileListHeader[] = {

	"# .dbl files to be included in the uber file. ORDER MATTERS! since we collapse the entire uber file down over chunk",
	"# data that gets discarded (like texture sets on ps2), we want files that get collapsed to come last in the uber file.",
	"# all non-collapse files should come first, then the remaining files should be roughly ordered by file size",
	"",
	"##################",
	"# this list is missing .dba sound files, i don't have them",
	"##################",
	"",
	"DBL_FILES = \\",
	NULL};	// must be last

char *MakefileRule1[] = {

	"",
	"# build an uber file out of component .dbl files, then clean up intermediate files",
	"",
	"$(TARGET_FILE) : $(DBL_FILES) makefile",
	"	$(LFB)",
	"	$(TOOLS)\\DBLMerge\\DBLMerge -UBERDBL -PARAMLIST $(DBL_FILES) $(TARGET_FILE)",
	NULL};	// must be last

char *MakefileRule2[] = {

	"	$(LFE)",
	"	",
	"",
	".PHONY: clean",
	"clean	:",
	"	$(RM) $(TARGET_FILE)",
	NULL};	// must be last

/******************* local prototypes ********************/

static char *BuildMakefile(char *pText, char *pWorldName, int nPlatform);
static void SkipWhiteSpace(char **ppText);
static void SkipLine(char **ppText);
static int FileCompare(const void *arg1, const void *arg2);
static void AddTextToDialog(const char *pText);
static bool CALLBACK DialogProc(HWND hDialog, UINT msg, WPARAM wParam, LPARAM lParam);

static int RecordCompare(const void *arg1, const void *arg2);	// for sn link map analysis code

/******************* functions ***************************/

int APIENTRY WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR     lpCmdLine,
                     int       nCmdShow)
{
#if 0
	/*** ned says: ok, this doesn't belong here at all...it's code to interpret a PS2 SN link map and spit out some ***/
	/*** statistics about how big things are. it's more or less accurate, but it doesn't completely account for the ***/
	/*** link map-- there are some header entries that say they use more memory than the sum of their sub-entries,  ***/
	/*** so there's something else going on there, but this code gets us as far as we need to go, so i'll leave it  ***/

	/* open source file */

	FILE *pMapFile = fopen("c:\\temp\\SLUS_20519.map", "rt");
	if (pMapFile == NULL)
		return(-1);
	fseek(pMapFile, 0, SEEK_END);
	int nFileSize = ftell(pMapFile);
	fseek(pMapFile, 0, SEEK_SET);
	char *pMapBuffer = (char *) malloc(nFileSize + 1);	// malloc an extra byte to guarantee buffer ends with a newline
	if (pMapBuffer == NULL)
		return(-2);
	fread(pMapBuffer, 1, nFileSize, pMapFile);
	fclose(pMapFile);
	pMapFile = NULL;

	/* make sure buffer is terminated with a newline so SkipLine call is always valid */

	pMapBuffer[nFileSize] = 10;

	/* read in lines */

	ts_Record *pRecords = NULL;
	int nMaxRecordCount = 0;
	int nRecordCount = 0;
	char *pNextMapText = pMapBuffer;
	char *pCurrentSegment = NULL;
	while (pNextMapText - pMapBuffer < nFileSize)
	{
		/* skip to next line */

		char *pMapText = pNextMapText;
		SkipLine(&pNextMapText);
		pNextMapText[-1] = 0;	// null-terminate pMapText

		/* process this line? */

		if (strncmp(pMapText, "00", 2) != 0)
			continue;

		/* read in data */

		ts_Record Record;
		sscanf(pMapText, "%x", &Record.nAddress);
		pMapText += 8;
		SkipWhiteSpace(&pMapText);
		sscanf(pMapText, "%x", &Record.nSize);
		if (Record.nSize == 0)
			continue;	// don't care about 0-sized items
		pMapText += 8;
		SkipWhiteSpace(&pMapText);

		/* skip alignment */

		while (*pMapText > ' ')
			pMapText++;

		/* maybe a new segment */

		if (strncmp(pMapText, " .", 2) == 0)
		{
			pMapText++;
			pCurrentSegment = pMapText;
			while (*pMapText > ' ')
				pMapText++;
			*pMapText = 0;	// null terminate segment
			continue;
		}

		/* is this a line we're interested in? */

		if (strncmp(pMapText, "                         ", 25) != 0)
			continue;

		/* record current segment (e.g. ".text") */

		Record.pSegment = pCurrentSegment;

		/* record class, if any */

		SkipWhiteSpace(&pMapText);
		char *pDelimiter = strstr(pMapText, "::");
		if (pDelimiter == NULL)
			pDelimiter = strstr(pMapText, "__");
		if (pDelimiter)
		{
			*pDelimiter = 0;	// null-terminate class name
			Record.pClass = pMapText;
			pMapText = pDelimiter + 2;
		}
		else
		{
			static char *pNoClass = "<No class>";
			Record.pClass = pNoClass;
		}

		/* record name (typically function name) */

		Record.pName = pMapText;

		/* don't know cumulative sizes yet */

		Record.nSegmentSize = 0;
		Record.nClassSize = 0;

		/* add this one to the list. may have to make some more room */

		if (nRecordCount >= nMaxRecordCount)
		{
			nMaxRecordCount += 1000;
			ts_Record *pNewRecords = (ts_Record *) malloc(nMaxRecordCount * sizeof(ts_Record));
			if (pNewRecords == NULL)
				return(-3);
			if (pRecords)
			{
				memcpy(pNewRecords, pRecords, nRecordCount * sizeof(ts_Record));
				free(pRecords);
			}
			pRecords = pNewRecords;
		}
		pRecords[nRecordCount++] = Record;
	}

	/* initial sort */

	qsort(pRecords, nRecordCount, sizeof(ts_Record), RecordCompare);

	/* calculate cumulative sizes */

	int nIndex = 0;
	int nSegmentStartIndex = 0;
	int nClassStartIndex = 0;
	int nSegmentSize = 0;
	int nClassSize = 0;
	pCurrentSegment = pRecords[nIndex].pSegment;
	char *pCurrentClass = pRecords[nIndex].pClass;
	while (nIndex < nRecordCount)
	{
		/* check for segment change */

		if (strcmp(pCurrentSegment, pRecords[nIndex].pSegment) != 0)
		{
			/* segment changed. record cumulative size */

			for (int i = nSegmentStartIndex; i < nIndex; i++)
				pRecords[i].nSegmentSize = nSegmentSize;
			nSegmentStartIndex = nIndex;
			nSegmentSize = 0;
			pCurrentSegment = pRecords[nIndex].pSegment;
		}
		else
		{
			/* segment stayed the same, update cumulative size */

			nSegmentSize += pRecords[nIndex].nSize;
		}

		/* check for class change */

		if (strcmp(pCurrentClass, pRecords[nIndex].pClass) != 0)
		{
			/* class changed. record cumulative size */

			for (int i = nClassStartIndex; i < nIndex; i++)
				pRecords[i].nClassSize = nClassSize;
			nClassStartIndex = nIndex;
			nClassSize = 0;
			pCurrentClass = pRecords[nIndex].pClass;
		}
		else
		{
			/* class stayed the same, update cumulative size */

			nClassSize += pRecords[nIndex].nSize;
		}

		/* to next record */

		nIndex++;
	}

	/* re-sort now that cumulative sizes are known */

	qsort(pRecords, nRecordCount, sizeof(ts_Record), RecordCompare);
	
	/* print out results */

	FILE *pLogFile = fopen("c:\\temp\\SLUS_20519.log", "wt");
	if (pLogFile == NULL)
		return(-4);
	for (int i = 0; i < nRecordCount; i++)
		fprintf(pLogFile, "%s\t%5d\t%s\t%5d\t%5d\t%s\n", pRecords[i].pSegment, pRecords[i].nSegmentSize, pRecords[i].pClass, pRecords[i].nClassSize, pRecords[i].nSize, pRecords[i].pName);
	fclose(pLogFile);
	pLogFile = NULL;

	/* success */

	if (pRecords)
		free(pRecords);
	free(pMapBuffer);
	return(0);
#endif //0

	/* can be run in batch mode and window told to close */

	bQuit = false;
	if (stricmp(lpCmdLine, "nowait") == 0)
		bQuit = true;

	/* start up dialog */

	hDialog = CreateDialog(hInstance, MAKEINTRESOURCE(IDD_PROGRESS), GetDesktopWindow(), (DLGPROC) DialogProc);
	int nProgramResult = 0;

	/* we must have environment variable that points to juju directory */

	int nReturn = GetEnvironmentVariable("JUJU_IMAGE_DIR", JujuPath, _MAX_PATH);
	if (nReturn == 0)
	{
		AddTextToDialog("Error: Must have environment variable JUJU_IMAGE_DIR set.\r\n");
		AddTextToDialog("Questions? ask Ned.\r\n");
		bQuit = false;	// make them look at error
		nProgramResult = -1;
	}

	/* still ok? */

	if (nProgramResult == 0)
	{
		/* each platform has it's own uberfile log. do them all */

		for (int nPlatform = 0; nPlatform < MAX_PLATFORMS; nPlatform++)
		{
			/* uberfile log is found in the appropriate platform-dependent directory */

			switch(nPlatform)
			{
			case PLATFORM_WIN32:
				strcpy(DataFolder, "data_pc");
				break;
			case PLATFORM_PS2:
				strcpy(DataFolder, "data_ps2");
				break;
			case PLATFORM_GCN:
				strcpy(DataFolder, "data_gcn");
				break;
			default:
				continue;
			}

			/* is there a file there? no error if not */

			char FileName[200];
			sprintf(FileName, "%s\\%s\\uberfile.log", JujuPath, DataFolder);
			FILE *pFile = fopen(FileName, "rt");
			if (pFile == NULL)
				continue;
			sprintf(DialogText, "Processing log file %s.\r\n", FileName);
			AddTextToDialog(DialogText);

			/* read it in */

			fseek(pFile, 0, SEEK_END);	// to end of file
			int nSize = ftell(pFile);
			fseek(pFile, 0, SEEK_SET);	// back to beginning
			char *pBuffer = (char *) malloc(nSize + 1);
			fread(pBuffer, nSize, 1, pFile);
			fclose(pFile);
			pBuffer[nSize] = 0;	// make sure it's 0-terminated

			/* start processing */

			char *pText = pBuffer;
			char Separators[] = " ,\t\n\0";
			while ((pText) && (*pText != 0))
			{
				char *pToken = strtok(pText, Separators);
				pText += strlen(pToken) + 1;	// skip over token just read

				/* comment? */

				if (strncmp(pToken, "//", 2) == 0)
				{
					SkipLine(&pText);
					continue;
				}

				/* version? */

				if (stricmp(pToken, "version") == 0)
				{
					int nVersion = atoi(pText);
					if (nVersion != VERSION_ID)
					{
						sprintf(DialogText, "Error: wrong version number-- expecting version %d.\r\n", VERSION_ID);
						AddTextToDialog(DialogText);
						AddTextToDialog("Questions? ask Ned.\r\n");
						bQuit = false;	// make them look at error
						nProgramResult = -1;
						pText = NULL;	// abort processing of this log
					}
					SkipLine(&pText);
					continue;
				}

				/* world? */

				if (stricmp(pToken, "world") == 0)
				{
					/* get name of world */

					char *pWorldName = strtok(pText, Separators);
					pText += strlen(pWorldName) + 1;	// skip over token just read

					/* compare next text to " no uberfile found". if it matches, then we are going to build a makefile for this world. */
					/* otherwise ignore text until next world */

					if (strnicmp(pText, " no uberfile found", strlen(" no uberfile found")) == 0)
					{
						SkipLine(&pText);
						pText = BuildMakefile(pText, pWorldName, nPlatform);
					}
					else
						SkipLine(&pText);
					continue;
				}

				/* unknown-- skip line and continue */

				SkipLine(&pText);
			}

			/* free uberfile log buffer */

			free(pBuffer);
		}
	}

	/* done processing */

	AddTextToDialog("Done.\r\n");

	/* wait for ok button (bQuit will already be true if in batch mode) */

	MSG msg;
	while ((bQuit == false) && (GetMessage(&msg, NULL, 0, 0)))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	if (hDialog)
		DestroyWindow(hDialog);

	/* return result-- 0 means success */

	return(nProgramResult);
}

/* build a makefile for a world. pText points to start of file list lines */

static char *BuildMakefile(char *pText, char *pWorldName, int nPlatform)
{
	/* output */

	sprintf(DialogText, "   Processing world %s.\r\n", pWorldName);
	AddTextToDialog(DialogText);

	/* make sure file list data is complete-- look for <end> in file. e.g. make sure they didn't quit out in the */
	/* middle of a world load */

	if (strstr(pText, "<end>") == NULL)
	{
		AddTextToDialog("      No <end> found, skipping world. Maybe didn't exit to fluff?\r\n");
		return(pText);
	}

	/* collect file lists */

	nFileCount = 0;
	while (strncmp(pText, "   <end>", strlen("   <end>")) != 0)
	{
		/* comment? */

		if ((pText[0] == '/') && (pText[1] == '/'))
		{
			SkipLine(&pText);
			continue;
		}

		/* start scanning files. file line is tab-delimited */

		char *pType = strtok(pText, "\t");
		pText += strlen(pType) + 1;	// skip over token just read
		char *pName = strtok(pText, "\t");
		pText += strlen(pName) + 1;	// skip over token just read
		char *pSize = strtok(pText, "\t");
		pText += strlen(pSize) + 1;	// skip over token just read
		char *pFlushedBytes = strtok(pText, "\t");
		pText += strlen(pFlushedBytes) + 1;	// skip over token just read
		char *pPlatformDependent = strtok(pText, "\n");
		pText += strlen(pPlatformDependent) + 1;	// skip over token just read

		/* lowercase file names for ease */

		_strlwr(pName);

		/* already in list? */

		for (int i = 0; i < nFileCount; i++)
		{
			if (strcmp(pName, Files[i].Name) == 0)
				break;	// found a duplicate
		}
		if (i < nFileCount)
			continue;	// found a duplicate

		/* add to list */

		assert(nFileCount < MAX_FILES_IN_LIST);
		ts_FileInfo *pFileInfo = &Files[nFileCount++];
		strcpy(pFileInfo->Name, pName);
		pFileInfo->nFileSize = atoi(pSize);
		pFileInfo->nFlushedBytes = atoi(pFlushedBytes);
		pFileInfo->bPlatformDependent = (*pPlatformDependent != '0');
		assert(pFileInfo->nFileSize != 0);	// bad data format

		/* file type */

		if (strstr(pType, "_dblParse") != NULL)
			pFileInfo->nType = DBL_FILE;
		else if (strstr(pType, "FileStream") != NULL)
			pFileInfo->nType = FILESTREAM_FILE;
		else
		{
			assert(false);	// unrecognized file type
			pFileInfo->nType = UNKNOWN_FILE;
		}

		/* there's always a special case-- .xml and .oa files are pre-built into data trees at */
		/* chunk processing time, and then flushed (most FileStream files are not flushed, and */
		/* the flushed bytes value in the uberfile log file is hardwired to 0) */

		if (pFileInfo->nType == FILESTREAM_FILE)
		{
			if ((strstr(pName, ".oa") != NULL) || (strstr(pName, ".xml") != NULL))
				pFileInfo->nFlushedBytes = pFileInfo->nFileSize;
		}
	}

	/* files that go into FileStream list need to have their extension translated. this is because the */
	/* file itself is not a .dbl file, but has to be wrapped into one by BuildDBL. the file uber.mk has */
	/* rules for calling BuildDBL. this will go away when we put the BuildDBL functionality into DBLMerge */

	ts_FileInfo *pFileInfo = Files;
	for (int i = 0; i < nFileCount; i++, pFileInfo++)
	{
		if (pFileInfo->nType == FILESTREAM_FILE)
		{
			char FileDrive[_MAX_DRIVE];
			char FileDir[_MAX_DIR];
			char FileRoot[_MAX_FNAME];
			char FileExt[_MAX_EXT];
			_splitpath(pFileInfo->Name, FileDrive, FileDir, FileRoot, FileExt);

			/* translate extension */

			static ts_Translate Translate[] = {

				{ ".fnt", ".dbf"},
				{ ".tbd", ".dsd"},
				{ ".tbl", ".dst"},
				{ ".ico", ".dbi"},
				{ ".xml", ".dbx"},
				{ ".oa",  ".doa"},
				{ ".cdb", ".dcd"},
				{ ".odb", ".dod"},
				{ ".ico", ".dbi"},
				{ ".mih", ".dmh"},
				{ ".var", ".dv"},		// not to be confused with a .dbv, which is compiled .var binary data

				/* must be last */

				{ NULL, NULL}};

			int i = 0;
			while (Translate[i].InputExt)
			{
				if (strcmp(FileExt, Translate[i].InputExt) == 0)
				{
					strcpy(FileExt, Translate[i].OutputExt);
					break;
				}
				i++;
			}
			assert(Translate[i].InputExt);	// if must find translation

			/* rebuild path */

			_makepath(pFileInfo->Name, FileDrive, FileDir, FileRoot, FileExt);
		}
	}
	sprintf(DialogText, "      %d files.\r\n", nFileCount);
	AddTextToDialog(DialogText);

	/* sort files. first come files that don't have any data flushed, then the rest are sorted by size, largest */
	/* first. this minimizes the amount of data that has to be memcpy'd by _dblCollapse */

	qsort(Files, nFileCount, sizeof(ts_FileInfo), FileCompare);

	/* create makefile */

	char FileName[_MAX_PATH];
	sprintf(FileName, "%s\\%s\\levels\\%s\\makefile", JujuPath, DataFolder, pWorldName);
	sprintf(DialogText, "      Creating %s.\r\n", FileName);
	AddTextToDialog(DialogText);
	int nReturn = _chmod(FileName, _S_IWRITE);	// make sure it's un-write-protected
	nReturn = remove(FileName);	// delete existing makefile, if any
	FILE *pMakefile = NULL;
	int nTries = 0;
	while ((pMakefile == NULL) && (nTries++ < 10))
	{
		pMakefile = fopen(FileName, "wt");
		if (pMakefile == NULL)
			Sleep(500);
	}
	assert(pMakefile);
	if (pMakefile)
	{
		/* note platform */

		switch(nPlatform)
		{
		case PLATFORM_WIN32:
			fprintf(pMakefile, "# WIN32 version\n");
			break;
		case PLATFORM_PS2:
			fprintf(pMakefile, "# PS2 version\n");
			break;
		case PLATFORM_GCN:
			fprintf(pMakefile, "# GCN version\n");
			break;
		}

		/* makefile header */

		int i = 0;
		while (MakefileHeader[i])
		{
			fprintf(pMakefile, "%s\n", MakefileHeader[i]);
			i++;
		}

		/* insert world name */

		fprintf(pMakefile, "WORLD = %s\n", pWorldName);

		/* makefile paths */

		i = 0;
		while (MakefilePaths[i])
		{
			fprintf(pMakefile, "%s\n", MakefilePaths[i]);
			i++;
		}

		/* file list header */

		i = 0;
		while (MakefileFileListHeader[i])
		{
			fprintf(pMakefile, "%s\n", MakefileFileListHeader[i]);
			i++;
		}

		/* file list */

		for (i = 0; i < nFileCount; i++)
		{
//			fprintf(pFile, "%s %d %d\n", pFiles[i].Name, pFiles[i].nFlushedBytes, pFiles[i].nFileSize);	// good for debugging sort
			if (Files[i].bPlatformDependent)
				fprintf(pMakefile, "	$(PD_PATH)\\%s", Files[i].Name);
			else
				fprintf(pMakefile, "	$(PI_PATH)\\%s", Files[i].Name);
			if (i < nFileCount - 1)
				fprintf(pMakefile, " \\");
			fprintf(pMakefile, "\n");
		}

		/* makefile rule, part 1 */

		i = 0;
		while (MakefileRule1[i])
		{
			fprintf(pMakefile, "%s\n", MakefileRule1[i]);
			i++;
		}

		/* files to remove, if any (clean up) */

		for (i = 0; i < nFileCount; i++)
		{
			if (Files[i].nType == FILESTREAM_FILE)
			{
				if (Files[i].bPlatformDependent)
					fprintf(pMakefile, "	$(RM) $(PD_PATH)\\%s\n", Files[i].Name);
				else
					fprintf(pMakefile, "	$(RM) $(PI_PATH)\\%s\n", Files[i].Name);
			}
		}

		/* makefile rule, part 2 */

		i = 0;
		while (MakefileRule2[i])
		{
			fprintf(pMakefile, "%s\n", MakefileRule2[i]);
			i++;
		}

		/* files to remove, again, as part of clean rule */

		for (i = 0; i < nFileCount; i++)
		{
			if (Files[i].nType == FILESTREAM_FILE)
			{
				if (Files[i].bPlatformDependent)
					fprintf(pMakefile, "	$(RM) $(PD_PATH)\\%s\n", Files[i].Name);
				else
					fprintf(pMakefile, "	$(RM) $(PI_PATH)\\%s\n", Files[i].Name);
			}
		}

		/* close makefile */

		fclose(pMakefile);
	}

	/* done */

	SkipLine(&pText);
	return(pText);
}

/* skip white space */

static void SkipWhiteSpace(char **ppText)
{
	while ((**ppText <= ' ') && (**ppText != 0))
		(*ppText)++;
}

/* skip to beginning of next line */

#define NEWLINE							10
#define CARRIAGE_RETURN					13
static void SkipLine(char **ppText)
{
	while ((**ppText != NEWLINE) && (**ppText != 0))
		(*ppText)++;
	while (((**ppText == NEWLINE) || (**ppText == CARRIAGE_RETURN)) && (**ppText != 0))
		(*ppText)++;
}

/* qsort comparison function */

static int FileCompare(const void *arg1, const void *arg2)
{
	ts_FileInfo *pFile1 = (ts_FileInfo *) arg1;
	ts_FileInfo *pFile2 = (ts_FileInfo *) arg2;

	/* some types of files are always last, so they can be free'd by just reducing the uberfile's memory */
	/* buffer size. these files are not flushed during .dbl chunk fix-up, but are no longer needed after */
	/* the world initialization is done. the example that i am implementing this for is .env binary files. */
	/* they can't be flushed because they are needed for initialization, but after initialization is done */
	/* their memory can be reclaimed */

	static char *pFileIsLast[] = {".env", ".dbv", ""};
	char FileExt[_MAX_EXT];
	int i = 0;

	/* is file 1 on the comes-last list? */

	_splitpath(pFile1->Name, NULL, NULL, NULL, FileExt);
	i = 0;
	while ((pFileIsLast[i][0] != 0) && (stricmp(pFileIsLast[i], FileExt) != 0))
		i++;
	if (pFileIsLast[i][0] != 0)
		return(1);	// file 1 on comes-last list, so file 2 comes first

	/* is file 2 on the comes-last list? */

	_splitpath(pFile2->Name, NULL, NULL, NULL, FileExt);
	i = 0;
	while ((pFileIsLast[i][0] != 0) && (stricmp(pFileIsLast[i], FileExt) != 0))
		i++;
	if (pFileIsLast[i][0] != 0)
		return(-1);	// file 2 on comes-last list, so file 1 comes first

	/* first, files that don't get flushed come before files that do */

	if ((pFile1->nFlushedBytes == 0) && (pFile2->nFlushedBytes != 0))
		return(-1);	// file 1 comes first
	if ((pFile1->nFlushedBytes != 0) && (pFile2->nFlushedBytes == 0))
		return(1);	// file 2 comes first

	/* files that both have flushed bytes == 0, sort by name */

	if (pFile1->nFlushedBytes == 0)
		return(strcmp(pFile1->Name, pFile2->Name));

	/* files with flushed bytes, sort by file size (largest first) */

	return(pFile2->nFileSize - pFile1->nFileSize);
}

/* add text to buffer in edit box. stolen from terraintoola */

static void AddTextToDialog(const char *pText)
{
	/* get control */

	HWND pEditBox = GetDlgItem(hDialog, IDC_PROGRESS);
	if (pEditBox == NULL)
		return;

	// Put the selection at the end of the text
	int length = (int) SendMessage(pEditBox, WM_GETTEXTLENGTH, 0, 0);
	SendMessage(pEditBox, EM_SETSEL, (WPARAM)length, (LPARAM)-1);

	// submit it
	SendMessage(pEditBox, EM_REPLACESEL, 0, (LPARAM) pText);

	// Scroll into view
	SendMessage(pEditBox, EM_SCROLLCARET, 0, 0);
}

/* control the dialog window */

static bool CALLBACK DialogProc(HWND hDialog, UINT msg, WPARAM wParam, LPARAM lParam)
{
	switch (msg)
	{
		case WM_COMMAND:
			{
				switch (LOWORD(wParam))
				{
					case IDOK:
						bQuit = true;
						return(true);
				}
			}
			break;
		case WM_DESTROY:
		case WM_CLOSE:
		case WM_QUIT:
//		case WM_SYSCOMMAND:
			bQuit = true;
			break;
	}

	return(false);
}

/* qsort comparison function, for sn link map analysis code */

static int RecordCompare(const void *arg1, const void *arg2)
{
	ts_Record *pRecord1 = (ts_Record *) arg1;
	ts_Record *pRecord2 = (ts_Record *) arg2;

	/* first sort by segment cumulative size (all members of same segment should have same cumulative size) */

	if (pRecord1->nSegmentSize != pRecord2->nSegmentSize)
		return(pRecord2->nSegmentSize - pRecord1->nSegmentSize);

	/* then by segment */

	int nCompare = strcmp(pRecord1->pSegment, pRecord2->pSegment);
	if (nCompare != 0)
		return(nCompare);

	/* then by class size */
	
	if (pRecord1->nClassSize != pRecord2->nClassSize)
		return(pRecord2->nClassSize - pRecord1->nClassSize);

	/* then by class */

	nCompare = strcmp(pRecord1->pClass, pRecord2->pClass);
	if (nCompare != 0)
		return(nCompare);

	/* then by size */

	return(pRecord2->nSize - pRecord1->nSize);
}

